1022 lines
38 KiB
Plaintext
1022 lines
38 KiB
Plaintext
//+------------------------------------------------------------------+
|
||
//| MssEntry.mqh — Order management cho MSS pipeline |
|
||
//+------------------------------------------------------------------+
|
||
//| Trách nhiệm: tính SL/TP/lot, đặt + sửa + huỷ pending limit, |
|
||
//| theo dõi position để partial close + BE + cancel. |
|
||
//| |
|
||
//| Gating (từ ngoài vào trong): |
|
||
//| gate1: InpMssTradeEnabled |
|
||
//| gate2: phase >= CHOCH && chochLocked && chochKeyLevel > 0 |
|
||
//| gate3: !intradayBlock |
|
||
//| intradayBlock = InpMssRequireIntradayAligned |
|
||
//| && !isAllowTrade (v1.181) |
|
||
//| gate4: !InpMssOnePosition || no existing position+pending |
|
||
//| |
|
||
//| ComputeLevels (v1.164 + v1.184): |
|
||
//| side ← (m5Zone.side) hoặc chochBias |
|
||
//| SL ← max(H0,H1)+buf (BEAR) / min(L0,L1)-buf (BULL) |
|
||
//| buf = InpMssSlSpreadMult × spread |
|
||
//| 2 candidates entry: |
|
||
//| A) M5 FVG limit price |
|
||
//| B) MSS keyLV (chochKeyLevel) |
|
||
//| ⇒ chọn cái có risk = |entry-SL| nhỏ hơn (đúng phía thị trường) |
|
||
//| TP target = iH0/iL0 (sóng H1): |
|
||
//| RR(TP@swing) > InpMssMinRR ⇒ TP @ swing - InpMssTpSpreadMult |
|
||
//| else ⇒ TP = entry ± risk × InpMssMinRR (2R fixed) |
|
||
//| partialTrigger = swingTp nếu TP gồng xa hơn swing |
|
||
//| ⇒ partial 50% + SL→BE khi giá đạt swingTp |
|
||
//| |
|
||
//| CHECK CUỐI (v1.184): |
|
||
//| |entry - market| ≤ InpMssMaxLimitDistAtrMult × ATR(FvgTf) |
|
||
//| Quá xa ⇒ skip + mark FVG used + reset state |
|
||
//| |
|
||
//| Lot sizing: IctMssEntry_VolumeForRisk uses InpMssRiskPct × balance|
|
||
//| |
|
||
//| Per-tick checks (gọi trong IctMssEntry_Update): |
|
||
//| IctMssEntry_CheckEodCancel(sym) — EOD phiên Mỹ |
|
||
//| IctMssEntry_CheckPendingTimeout(sym) — timeout 1h |
|
||
//| IctMssEntry_CheckTpReachedBeforeFill(sym) — TP chạm trước fill|
|
||
//| (v1.185) |
|
||
//| IctMssEntry_CheckStaleLimit(sym) — limit xa giá |
|
||
//| > N×ATR (v1.184)|
|
||
//| IctMssEntry_CheckBreakevenAtRR(sym) — SL→entry @ N×R |
|
||
//| (v1.176-7, off) |
|
||
//| IctMssEntry_CheckPartialClose(sym) — partial 50% + |
|
||
//| SL→BE @ swingTp |
|
||
//| |
|
||
//| Globals owned: |
|
||
//| g_ictMssTrade — CTrade instance dùng cho tất cả order ops |
|
||
//| |
|
||
//| Lifecycle: |
|
||
//| IctMss_OnPositionClosed (gọi từ OnTradeTransaction trong EA): |
|
||
//| - MarkM5FvgUsed |
|
||
//| - IctMss_ResetState |
|
||
//| - set g_ictMssAfterCloseGuard = TimeCurrent |
|
||
//| - reset về WAIT_FVG_TOUCH |
|
||
//| |
|
||
//| Public API (chính): |
|
||
//| void IctMssEntry_Init() / IctMssEntry_Update(sym) |
|
||
//| bool IctMssEntry_ComputeLevels(sym, m5Zone, &entry, &sl, &tp, |
|
||
//| &reasonOut) |
|
||
//| void IctMss_OnPositionClosed(sym, reason, netProfit) |
|
||
//+------------------------------------------------------------------+
|
||
#ifndef ICT2026_MSSENTRY_MQH
|
||
#define ICT2026_MSSENTRY_MQH
|
||
|
||
#include <Trade/Trade.mqh>
|
||
#include <ICT2026/Journal.mqh>
|
||
#include <ICT2026/MssSetup.mqh>
|
||
#include <ICT2026/IntradayStructure.mqh>
|
||
|
||
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_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;
|
||
}
|
||
|
||
// Cap khoảng cách entry-giá theo bội số ATR(FvgTf):
|
||
// BUY ⇒ distance = Ask - entry (entry phải nằm dưới Ask cho limit buy)
|
||
// SELL ⇒ distance = entry - Bid (entry phải nằm trên Bid cho limit sell)
|
||
// Trả về true nếu distance > InpMssMaxLimitDistAtrMult × ATR ⇒ "limit quá xa" (chết).
|
||
bool IctMssEntry_IsLimitTooFar(const string sym, const double entry, const bool isBuy,
|
||
double &distOut, double &thresholdOut)
|
||
{
|
||
distOut = 0.0;
|
||
thresholdOut = 0.0;
|
||
if(InpMssMaxLimitDistAtrMult <= 0.0)
|
||
return false;
|
||
|
||
const double atr = IctFvg_AtrFvgTf(sym);
|
||
if(atr <= 0.0)
|
||
return false;
|
||
|
||
const double mkt = isBuy ? SymbolInfoDouble(sym, SYMBOL_ASK)
|
||
: SymbolInfoDouble(sym, SYMBOL_BID);
|
||
if(mkt <= 0.0 || entry <= 0.0)
|
||
return false;
|
||
|
||
distOut = isBuy ? (mkt - entry) : (entry - mkt);
|
||
thresholdOut = atr * InpMssMaxLimitDistAtrMult;
|
||
return (distOut > thresholdOut + _Point);
|
||
}
|
||
|
||
bool IctMssEntry_ComputeLevels(const string sym, const IctFvgZone &m5Zone,
|
||
double &entryOut, double &slOut, double &tpOut,
|
||
string &reasonOut)
|
||
{
|
||
entryOut = slOut = tpOut = 0.0;
|
||
reasonOut = "";
|
||
|
||
// ── Xác định side: ưu tiên M5 FVG, fallback chochBias khi không có FVG
|
||
const bool hasFvg = (m5Zone.side != ICT_FVG_NONE);
|
||
ENUM_ICT_BIAS bias = ICT_BIAS_NONE;
|
||
if(hasFvg)
|
||
bias = (m5Zone.side == ICT_FVG_BULL) ? ICT_BIAS_BULL : ICT_BIAS_BEAR;
|
||
else if(g_ictLowTf.mss.chochLocked)
|
||
bias = g_ictLowTf.mss.chochBias;
|
||
|
||
if(bias == ICT_BIAS_NONE)
|
||
{
|
||
reasonOut = "Không xác định được side (no FVG + no chochBias)";
|
||
return false;
|
||
}
|
||
const bool isBuy = (bias == ICT_BIAS_BULL);
|
||
|
||
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
|
||
{
|
||
reasonOut = "Chưa có swing H0/L0 M5 cho SL";
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Buffer SL/TP thuần spread — ổn định, không lệ thuộc ATR động
|
||
// SL = swing + InpMssSlSpreadMult × spread (cộng ra ngoài đỉnh/đáy)
|
||
// TP = iL0/iH0 ± InpMssTpSpreadMult × spread (chốt trước vùng cản để dễ khớp)
|
||
const double spreadNow = MathMax(0.0,
|
||
SymbolInfoDouble(sym, SYMBOL_ASK) -
|
||
SymbolInfoDouble(sym, SYMBOL_BID));
|
||
const double spreadUnit = (spreadNow > 0.0) ? spreadNow : _Point;
|
||
|
||
const double bufSl = InpMssSlSpreadMult * spreadUnit;
|
||
const double bufTp = InpMssTpSpreadMult * spreadUnit;
|
||
const double minRR = MathMax(1.0, InpMssMinRR);
|
||
|
||
// SL final (đã buffer + spread)
|
||
slOut = isBuy ? swingSl - bufSl : swingSl + bufSl;
|
||
|
||
// ── 2 candidate entries:
|
||
// A) M5 FVG limit (nếu có)
|
||
// B) MSS keyLV (chochKeyLevel — L0 broken cho bear, H0 broken cho bull)
|
||
const double mktBid = SymbolInfoDouble(sym, SYMBOL_BID);
|
||
const double mktAsk = SymbolInfoDouble(sym, SYMBOL_ASK);
|
||
|
||
double entryFvg = 0.0, riskFvg = DBL_MAX;
|
||
if(hasFvg)
|
||
{
|
||
entryFvg = IctMssEntry_LimitPrice(m5Zone);
|
||
const double rA = isBuy ? (entryFvg - slOut) : (slOut - entryFvg);
|
||
const bool limitOk = isBuy ? (entryFvg < mktAsk - _Point)
|
||
: (entryFvg > mktBid + _Point);
|
||
if(entryFvg > 0.0 && rA > _Point && limitOk)
|
||
riskFvg = rA;
|
||
}
|
||
|
||
double entryMss = g_ictLowTf.mss.chochKeyLevel;
|
||
double riskMss = DBL_MAX;
|
||
{
|
||
const double rB = isBuy ? (entryMss - slOut) : (slOut - entryMss);
|
||
const bool limitOk = isBuy ? (entryMss < mktAsk - _Point)
|
||
: (entryMss > mktBid + _Point);
|
||
if(entryMss > 0.0 && rB > _Point && limitOk)
|
||
riskMss = rB;
|
||
}
|
||
|
||
string entrySource = "";
|
||
if(riskFvg < DBL_MAX && (riskMss == DBL_MAX || riskFvg < riskMss))
|
||
{
|
||
entryOut = entryFvg;
|
||
entrySource = StringFormat("M5 FVG (risk %.2f < MSS %.2f)",
|
||
riskFvg, (riskMss == DBL_MAX ? 0.0 : riskMss));
|
||
}
|
||
else if(riskMss < DBL_MAX)
|
||
{
|
||
entryOut = entryMss;
|
||
if(hasFvg)
|
||
entrySource = StringFormat("MSS keyLV (risk %.2f ≤ FVG %.2f)",
|
||
riskMss, (riskFvg == DBL_MAX ? 999.99 : riskFvg));
|
||
else
|
||
entrySource = "MSS keyLV (no M5 FVG)";
|
||
}
|
||
else
|
||
{
|
||
reasonOut = StringFormat(
|
||
"Cả 2 entry không hợp lệ | mkt=%.2f/%.2f | FVG=%.2f | MSS=%.2f | SL=%.2f",
|
||
mktBid, mktAsk, entryFvg, entryMss, slOut);
|
||
return false;
|
||
}
|
||
|
||
// Cap khoảng cách entry-giá: tránh đặt limit "chết" sau khi MSS confirm muộn
|
||
// ⇒ limit phải nằm trong InpMssMaxLimitDistAtrMult × ATR(FvgTf) tính từ giá hiện tại
|
||
double tooFarDist = 0.0, tooFarThresh = 0.0;
|
||
if(IctMssEntry_IsLimitTooFar(sym, entryOut, isBuy, tooFarDist, tooFarThresh))
|
||
{
|
||
reasonOut = StringFormat(
|
||
"Entry xa giá %.0f pts > %.0f pts (%.1f×ATR) — bỏ qua [src=%s]",
|
||
tooFarDist / _Point, tooFarThresh / _Point,
|
||
InpMssMaxLimitDistAtrMult, entrySource);
|
||
return false;
|
||
}
|
||
|
||
// ── TP target = iL0 (BEAR) / iH0 (BULL): sóng H1, m5 chỉ để entry
|
||
// TP đặt CÁCH target một buffer (gần chạm — không chờ hit chính xác)
|
||
const IctSwingSet iSw = g_ictIntraday.swings;
|
||
double tpTarget = 0.0;
|
||
if(isBuy && iSw.hasH0)
|
||
tpTarget = iSw.h0.price;
|
||
else if(!isBuy && iSw.hasL0)
|
||
tpTarget = iSw.l0.price;
|
||
|
||
// ── Logic TP (1 ngưỡng duy nhất):
|
||
// RR(TP@iL0/iH0) > InpMssMinRR (= 2.0) → dùng TP tại iL0/iH0 (mục tiêu sóng H1)
|
||
// Ngược lại (RR ≤ 2.0 hoặc iL0/iH0 không khả dụng) → TP cố định = entry ± risk × InpMssMinRR (= 2R)
|
||
const double risk = isBuy ? (entryOut - slOut) : (slOut - entryOut);
|
||
if(risk <= _Point)
|
||
{
|
||
reasonOut = StringFormat("Risk≤0 (SL %.5f vs entry %.5f)", slOut, entryOut);
|
||
return false;
|
||
}
|
||
|
||
// Swing-based TP (= TP "tự nhiên" tại iL0/iH0 ± bufTp) — dùng cho partial close trigger
|
||
double swingTp = 0.0;
|
||
if(isBuy)
|
||
{
|
||
double tpAtIH0 = 0.0, rrAtIH0 = 0.0;
|
||
if(tpTarget > 0.0 && tpTarget > entryOut + bufTp + _Point)
|
||
{
|
||
tpAtIH0 = tpTarget - bufTp;
|
||
rrAtIH0 = (tpAtIH0 - entryOut) / risk;
|
||
swingTp = tpAtIH0;
|
||
}
|
||
tpOut = (rrAtIH0 > minRR) ? tpAtIH0 : (entryOut + risk * minRR);
|
||
}
|
||
else
|
||
{
|
||
double tpAtIL0 = 0.0, rrAtIL0 = 0.0;
|
||
if(tpTarget > 0.0 && tpTarget < entryOut - bufTp - _Point)
|
||
{
|
||
tpAtIL0 = tpTarget + bufTp;
|
||
rrAtIL0 = (entryOut - tpAtIL0) / risk;
|
||
swingTp = tpAtIL0;
|
||
}
|
||
tpOut = (rrAtIL0 > minRR) ? tpAtIL0 : (entryOut - risk * minRR);
|
||
}
|
||
|
||
// Partial close trigger: chỉ kích hoạt khi TP gồng XA HƠN swing TP
|
||
// BUY: tpOut > swingTp + _Point → partial khi giá lên đến swingTp
|
||
// SELL: tpOut < swingTp - _Point → partial khi giá xuống đến swingTp
|
||
double partialTrigger = 0.0;
|
||
if(swingTp > 0.0 && InpMssPartialClosePct > 0.0)
|
||
{
|
||
if(isBuy && tpOut > swingTp + _Point)
|
||
partialTrigger = swingTp;
|
||
else if(!isBuy && tpOut < swingTp - _Point)
|
||
partialTrigger = swingTp;
|
||
}
|
||
g_ictLowTf.mss.partialTriggerPrice = partialTrigger;
|
||
g_ictLowTf.mss.partialCloseDone = false;
|
||
|
||
// entrySource lưu vào reasonOut để display thấy nguồn entry
|
||
reasonOut = entrySource;
|
||
|
||
entryOut = IctMssEntry_NormalizePrice(sym, entryOut);
|
||
slOut = IctMssEntry_NormalizePrice(sym, slOut);
|
||
tpOut = IctMssEntry_NormalizePrice(sym, tpOut);
|
||
|
||
if(isBuy && (slOut >= entryOut - _Point || tpOut <= entryOut + _Point))
|
||
{
|
||
reasonOut = StringFormat("SL/TP sai phía (BUY entry=%.5f SL=%.5f TP=%.5f)",
|
||
entryOut, slOut, tpOut);
|
||
return false;
|
||
}
|
||
if(!isBuy && (slOut <= entryOut + _Point || tpOut >= entryOut - _Point))
|
||
{
|
||
reasonOut = StringFormat("SL/TP sai phía (SELL entry=%.5f SL=%.5f TP=%.5f)",
|
||
entryOut, slOut, tpOut);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool IctMssEntry_HasOpenPositionMagic(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;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool IctMssEntry_HasOpenExposure(const string sym)
|
||
{
|
||
if(IctMssEntry_HasOpenPositionMagic(sym))
|
||
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;
|
||
}
|
||
|
||
string IctMssEntry_DealReasonText(const long r)
|
||
{
|
||
switch((ENUM_DEAL_REASON)r)
|
||
{
|
||
case DEAL_REASON_SL: return "SL";
|
||
case DEAL_REASON_TP: return "TP";
|
||
case DEAL_REASON_SO: return "Stop Out";
|
||
case DEAL_REASON_EXPERT: return "Expert close";
|
||
case DEAL_REASON_CLIENT: return "Manual";
|
||
case DEAL_REASON_MOBILE: return "Mobile";
|
||
case DEAL_REASON_WEB: return "Web";
|
||
default: return (r < 0) ? "Detected" : "Unknown";
|
||
}
|
||
}
|
||
|
||
void IctMssEntry_MarkM5FvgUsed(const string sym, const ulong fvgId)
|
||
{
|
||
if(fvgId == 0)
|
||
return;
|
||
const int idx = IctConfirmFvg_FindById(fvgId);
|
||
if(idx < 0)
|
||
return;
|
||
g_ictConfirmFvgZones[idx].state = ICT_FVG_USED;
|
||
if(g_ictConfirmFvgZones[idx].fvgUsedTime == 0)
|
||
g_ictConfirmFvgZones[idx].fvgUsedTime = iTime(sym, InpConfirmTf, 0);
|
||
}
|
||
|
||
void IctMss_OnEodCancel(const string sym, const ulong ticket)
|
||
{
|
||
const ulong m5Id = g_ictLowTf.mss.m5FvgId;
|
||
const ulong h1Id = g_ictLowTf.mss.h1FvgId;
|
||
|
||
if(ticket > 0 && OrderSelect(ticket))
|
||
g_ictMssTrade.OrderDelete(ticket);
|
||
|
||
IctMss_ResetState();
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"EOD cancel pending #%I64u | H1 #%I64u — reset, chờ phiên mới",
|
||
ticket, h1Id);
|
||
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] EOD cancel limit #%I64u | H1 #%I64u | M5 #%I64u | reset → WAIT_FVG_TOUCH",
|
||
ticket, h1Id, m5Id);
|
||
}
|
||
|
||
void IctMss_OnPendingTimeout(const string sym, const ulong ticket, const int hours)
|
||
{
|
||
const ulong m5Id = g_ictLowTf.mss.m5FvgId;
|
||
const ulong h1Id = g_ictLowTf.mss.h1FvgId;
|
||
|
||
if(ticket > 0 && OrderSelect(ticket))
|
||
g_ictMssTrade.OrderDelete(ticket);
|
||
|
||
IctMssEntry_MarkM5FvgUsed(sym, m5Id);
|
||
IctMss_ResetState();
|
||
|
||
// Guard giống OnPositionClosed: chỉ accept H1 FVG có touch sau timeout
|
||
g_ictMssAfterCloseGuard = TimeCurrent();
|
||
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"Timeout %dh pending #%I64u | M5 FVG #%I64u → Used | chờ touch FVG mới",
|
||
hours, ticket, m5Id);
|
||
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] Pending timeout %dh #%I64u | H1 #%I64u | M5 #%I64u → Used | guard=%s | reset → WAIT_FVG_TOUCH",
|
||
hours, ticket, h1Id, m5Id,
|
||
TimeToString(g_ictMssAfterCloseGuard, TIME_DATE | TIME_MINUTES));
|
||
}
|
||
|
||
// Cancel pending + mark FVG used + reset state khi limit rời xa giá vượt cap.
|
||
// ⇒ tránh case MSS confirm muộn → limit đặt rất xa hiện tại, không bao giờ khớp.
|
||
// Trigger:
|
||
// - Bị gọi mỗi tick trong IctMssEntry_Update
|
||
// - InpMssCancelStaleLimit phải bật
|
||
// - Có pendingTicket hợp lệ (đã đặt limit)
|
||
// - (mkt - entry) (BUY) hoặc (entry - mkt) (SELL) > InpMssMaxLimitDistAtrMult × ATR(FvgTf)
|
||
void IctMssEntry_CheckStaleLimit(const string sym)
|
||
{
|
||
if(!InpMssCancelStaleLimit)
|
||
return;
|
||
if(InpMssMaxLimitDistAtrMult <= 0.0)
|
||
return;
|
||
if(g_ictLowTf.mss.pendingTicket == 0)
|
||
return;
|
||
if(!OrderSelect(g_ictLowTf.mss.pendingTicket))
|
||
return;
|
||
|
||
const ENUM_ORDER_TYPE otype = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
|
||
if(otype != ORDER_TYPE_BUY_LIMIT && otype != ORDER_TYPE_SELL_LIMIT)
|
||
return;
|
||
|
||
const bool isBuy = (otype == ORDER_TYPE_BUY_LIMIT);
|
||
const double entry = OrderGetDouble(ORDER_PRICE_OPEN);
|
||
|
||
double dist = 0.0, thresh = 0.0;
|
||
if(!IctMssEntry_IsLimitTooFar(sym, entry, isBuy, dist, thresh))
|
||
return;
|
||
|
||
const ulong ticket = g_ictLowTf.mss.pendingTicket;
|
||
const ulong m5Id = g_ictLowTf.mss.m5FvgId;
|
||
const ulong h1Id = g_ictLowTf.mss.h1FvgId;
|
||
|
||
g_ictMssTrade.OrderDelete(ticket);
|
||
g_ictLowTf.mss.pendingTicket = 0;
|
||
IctMssEntry_MarkM5FvgUsed(sym, m5Id);
|
||
IctMss_ResetState();
|
||
g_ictMssAfterCloseGuard = TimeCurrent();
|
||
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"Limit chết: cách giá %.0f pts > %.0f pts (%.1f×ATR) — cancel #%I64u + reset",
|
||
dist / _Point, thresh / _Point, InpMssMaxLimitDistAtrMult, ticket);
|
||
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] Stale limit cancel %s #%I64u | dist=%.0f pts thresh=%.0f pts | H1 #%I64u | M5 #%I64u → Used | guard=%s | reset → WAIT_FVG_TOUCH",
|
||
isBuy ? "BUY" : "SELL", ticket,
|
||
dist / _Point, thresh / _Point, h1Id, m5Id,
|
||
TimeToString(g_ictMssAfterCloseGuard, TIME_DATE | TIME_MINUTES));
|
||
}
|
||
|
||
// Cancel pending limit khi giá chạm TP TRƯỚC khi limit khớp.
|
||
// ⇒ Tránh case: giá chạy thẳng đến TP target, không hồi lại entry, sau đó
|
||
// đảo chiều quay về khớp limit ở entry cũ → lệnh chạy ngược, SL.
|
||
// Logic:
|
||
// BUY limit: nếu Bid ≥ TP ⇒ giá đã chạm TP (lúc fill xong sẽ TP ngay)
|
||
// SELL limit: nếu Ask ≤ TP ⇒ giá đã chạm TP
|
||
// Sau cancel: mark FVG used + reset (giống stale limit) + guard sau-close.
|
||
void IctMssEntry_CheckTpReachedBeforeFill(const string sym)
|
||
{
|
||
if(!InpMssCancelLimitWhenTpReached)
|
||
return;
|
||
if(g_ictLowTf.mss.pendingTicket == 0)
|
||
return;
|
||
if(!OrderSelect(g_ictLowTf.mss.pendingTicket))
|
||
return;
|
||
|
||
const ENUM_ORDER_TYPE otype = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
|
||
if(otype != ORDER_TYPE_BUY_LIMIT && otype != ORDER_TYPE_SELL_LIMIT)
|
||
return;
|
||
|
||
const bool isBuy = (otype == ORDER_TYPE_BUY_LIMIT);
|
||
const double tp = OrderGetDouble(ORDER_TP);
|
||
if(tp <= 0.0)
|
||
return;
|
||
|
||
const double bid = SymbolInfoDouble(sym, SYMBOL_BID);
|
||
const double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
|
||
|
||
const bool reached = isBuy ? (bid >= tp - _Point)
|
||
: (ask <= tp + _Point);
|
||
if(!reached)
|
||
return;
|
||
|
||
const ulong ticket = g_ictLowTf.mss.pendingTicket;
|
||
const ulong m5Id = g_ictLowTf.mss.m5FvgId;
|
||
const ulong h1Id = g_ictLowTf.mss.h1FvgId;
|
||
const double entry = OrderGetDouble(ORDER_PRICE_OPEN);
|
||
|
||
g_ictMssTrade.OrderDelete(ticket);
|
||
g_ictLowTf.mss.pendingTicket = 0;
|
||
IctMssEntry_MarkM5FvgUsed(sym, m5Id);
|
||
IctMss_ResetState();
|
||
g_ictMssAfterCloseGuard = TimeCurrent();
|
||
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"Giá chạm TP trước khi khớp limit (TP=%.2f, %s=%.2f) — cancel #%I64u + reset",
|
||
tp, isBuy ? "Bid" : "Ask", isBuy ? bid : ask, ticket);
|
||
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] TP-reached cancel %s #%I64u | entry=%.5f tp=%.5f bid=%.5f ask=%.5f | H1 #%I64u | M5 #%I64u → Used | guard=%s | reset → WAIT_FVG_TOUCH",
|
||
isBuy ? "BUY" : "SELL", ticket,
|
||
entry, tp, bid, ask, h1Id, m5Id,
|
||
TimeToString(g_ictMssAfterCloseGuard, TIME_DATE | TIME_MINUTES));
|
||
}
|
||
|
||
// Dời SL về entry (BE) khi lệnh đi được N×R (mặc định 2R) — bảo toàn vốn sớm.
|
||
// - Không partial close (khác với CheckPartialClose ở swing iL0/iH0)
|
||
// - Skip nếu BE đã move (beMovedDone) hoặc partial close đã chạy (đã set BE)
|
||
void IctMssEntry_CheckBreakevenAtRR(const string sym)
|
||
{
|
||
if(!InpMssBeEnabled)
|
||
return;
|
||
if(InpMssBeAtRR <= 0.0)
|
||
return;
|
||
if(g_ictLowTf.mss.beMovedDone || g_ictLowTf.mss.partialCloseDone)
|
||
return;
|
||
if(g_ictLowTf.mss.pendingEntry <= 0.0 || g_ictLowTf.mss.pendingSl <= 0.0)
|
||
return;
|
||
|
||
const double entry = g_ictLowTf.mss.pendingEntry;
|
||
const double slOrig = g_ictLowTf.mss.pendingSl;
|
||
const double risk = MathAbs(entry - slOrig);
|
||
if(risk <= 0.0)
|
||
return;
|
||
|
||
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)
|
||
continue;
|
||
|
||
const ENUM_POSITION_TYPE ptype = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
||
const double openPx = PositionGetDouble(POSITION_PRICE_OPEN);
|
||
const double curSl = PositionGetDouble(POSITION_SL);
|
||
const double curTp = PositionGetDouble(POSITION_TP);
|
||
const double bid = SymbolInfoDouble(sym, SYMBOL_BID);
|
||
const double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
|
||
|
||
bool reached = false;
|
||
double trigPx = 0.0;
|
||
if(ptype == POSITION_TYPE_BUY)
|
||
{
|
||
trigPx = openPx + InpMssBeAtRR * risk;
|
||
reached = (bid >= trigPx - _Point);
|
||
}
|
||
else if(ptype == POSITION_TYPE_SELL)
|
||
{
|
||
trigPx = openPx - InpMssBeAtRR * risk;
|
||
reached = (ask <= trigPx + _Point);
|
||
}
|
||
|
||
if(!reached)
|
||
return;
|
||
|
||
// Skip nếu SL hiện tại đã ≥ BE (tránh dời lùi)
|
||
bool slAlreadyAtBe = false;
|
||
if(ptype == POSITION_TYPE_BUY)
|
||
slAlreadyAtBe = (curSl >= openPx - _Point);
|
||
else
|
||
slAlreadyAtBe = (curSl > 0.0 && curSl <= openPx + _Point);
|
||
|
||
if(slAlreadyAtBe)
|
||
{
|
||
g_ictLowTf.mss.beMovedDone = true;
|
||
return;
|
||
}
|
||
|
||
g_ictMssTrade.SetExpertMagicNumber(InpMssMagic);
|
||
if(!g_ictMssTrade.PositionModify(ticket, openPx, curTp))
|
||
{
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] BE@%.1fR move fail #%I64u — %d %s",
|
||
InpMssBeAtRR, ticket, g_ictMssTrade.ResultRetcode(),
|
||
g_ictMssTrade.ResultRetcodeDescription());
|
||
return;
|
||
}
|
||
|
||
g_ictLowTf.mss.beMovedDone = true;
|
||
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] BE@%.1fR #%I64u price=%.2f → SL %.2f→%.2f (entry) | TP %.2f",
|
||
InpMssBeAtRR, ticket,
|
||
(ptype == POSITION_TYPE_BUY ? bid : ask),
|
||
curSl, openPx, curTp);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Chốt 50% volume + dời SL về BE khi giá đạt swing iL0/iH0 (chỉ áp dụng khi TP gồng xa hơn swing)
|
||
void IctMssEntry_CheckPartialClose(const string sym)
|
||
{
|
||
if(InpMssPartialClosePct <= 0.0)
|
||
return;
|
||
if(g_ictLowTf.mss.partialCloseDone)
|
||
return;
|
||
if(g_ictLowTf.mss.partialTriggerPrice <= 0.0)
|
||
return;
|
||
|
||
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)
|
||
continue;
|
||
|
||
const ENUM_POSITION_TYPE ptype = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
||
const double openPx = PositionGetDouble(POSITION_PRICE_OPEN);
|
||
const double volume = PositionGetDouble(POSITION_VOLUME);
|
||
const double curTp = PositionGetDouble(POSITION_TP);
|
||
const double bid = SymbolInfoDouble(sym, SYMBOL_BID);
|
||
const double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
|
||
const double trig = g_ictLowTf.mss.partialTriggerPrice;
|
||
|
||
bool reached = false;
|
||
if(ptype == POSITION_TYPE_BUY)
|
||
reached = (bid >= trig - _Point);
|
||
else if(ptype == POSITION_TYPE_SELL)
|
||
reached = (ask <= trig + _Point);
|
||
|
||
if(!reached)
|
||
return;
|
||
|
||
// Tính volume cần đóng (1 nửa, đã normalize step)
|
||
const double pct = MathMax(0.0, MathMin(100.0, InpMssPartialClosePct)) / 100.0;
|
||
const double vClose = IctMssEntry_NormalizeVolume(sym, volume * pct);
|
||
if(vClose <= 0.0 || vClose >= volume)
|
||
return;
|
||
|
||
g_ictMssTrade.SetExpertMagicNumber(InpMssMagic);
|
||
if(!g_ictMssTrade.PositionClosePartial(ticket, vClose))
|
||
{
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] PartialClose fail #%I64u vol=%.2f — %d %s",
|
||
ticket, vClose, g_ictMssTrade.ResultRetcode(),
|
||
g_ictMssTrade.ResultRetcodeDescription());
|
||
return;
|
||
}
|
||
|
||
// Dời SL về BE (entry price). Giữ TP cũ.
|
||
if(!g_ictMssTrade.PositionModify(ticket, openPx, curTp))
|
||
{
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] BE move fail #%I64u — %d %s",
|
||
ticket, g_ictMssTrade.ResultRetcode(),
|
||
g_ictMssTrade.ResultRetcodeDescription());
|
||
}
|
||
|
||
g_ictLowTf.mss.partialCloseDone = true;
|
||
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] PartialClose #%I64u %.0f%% (%.2f→%.2f lot) @ %.2f | SL→BE %.2f | TP %.2f",
|
||
ticket, InpMssPartialClosePct, volume, volume - vClose,
|
||
(ptype == POSITION_TYPE_BUY ? bid : ask),
|
||
openPx, curTp);
|
||
return;
|
||
}
|
||
}
|
||
|
||
void IctMssEntry_CheckPendingTimeout(const string sym)
|
||
{
|
||
if(InpMssPendingExpireHours <= 0)
|
||
return;
|
||
if(g_ictLowTf.mss.pendingTicket == 0)
|
||
return;
|
||
if(g_ictLowTf.mss.pendingPlacedTime == 0)
|
||
return;
|
||
if(IctMssEntry_HasOpenPositionMagic(sym))
|
||
return;
|
||
|
||
const ulong ticket = g_ictLowTf.mss.pendingTicket;
|
||
if(!OrderSelect(ticket))
|
||
{
|
||
// Pending đã biến mất (broker expire / cancel manual) mà chưa thành position
|
||
// → vẫn coi như timeout, reset state để chờ FVG mới
|
||
IctMss_OnPendingTimeout(sym, 0, InpMssPendingExpireHours);
|
||
return;
|
||
}
|
||
|
||
const datetime placed = g_ictLowTf.mss.pendingPlacedTime;
|
||
const int elapsedSec = (int)(TimeCurrent() - placed);
|
||
const int timeoutSec = InpMssPendingExpireHours * 3600;
|
||
if(elapsedSec < timeoutSec)
|
||
return;
|
||
|
||
IctMss_OnPendingTimeout(sym, ticket, InpMssPendingExpireHours);
|
||
}
|
||
|
||
void IctMssEntry_CheckEodCancel(const string sym)
|
||
{
|
||
if(!InpMssCancelPendingEod)
|
||
return;
|
||
if(g_ictLowTf.mss.pendingTicket == 0)
|
||
return;
|
||
if(IctMssEntry_HasOpenPositionMagic(sym))
|
||
return;
|
||
|
||
const ulong ticket = g_ictLowTf.mss.pendingTicket;
|
||
if(!OrderSelect(ticket))
|
||
{
|
||
g_ictLowTf.mss.pendingTicket = 0;
|
||
return;
|
||
}
|
||
|
||
MqlDateTime mdt;
|
||
TimeToStruct(TimeCurrent(), mdt);
|
||
const int nowMinutes = mdt.hour * 60 + mdt.min;
|
||
const int eodMinutes = InpMssEodHour * 60 + InpMssEodMinute;
|
||
if(nowMinutes < eodMinutes)
|
||
return;
|
||
|
||
MqlDateTime d0;
|
||
d0.year = mdt.year;
|
||
d0.mon = mdt.mon;
|
||
d0.day = mdt.day;
|
||
d0.hour = 0;
|
||
d0.min = 0;
|
||
d0.sec = 0;
|
||
const datetime dayStart = StructToTime(d0);
|
||
|
||
static datetime s_lastEodHandledDate = 0;
|
||
if(s_lastEodHandledDate == dayStart)
|
||
return;
|
||
s_lastEodHandledDate = dayStart;
|
||
|
||
IctMss_OnEodCancel(sym, ticket);
|
||
}
|
||
|
||
void IctMss_OnPositionClosed(const string sym, const long reason, const double netProfit)
|
||
{
|
||
const ulong m5Id = g_ictLowTf.mss.m5FvgId;
|
||
const ulong h1Id = g_ictLowTf.mss.h1FvgId;
|
||
const string rt = IctMssEntry_DealReasonText(reason);
|
||
|
||
IctMssEntry_MarkM5FvgUsed(sym, m5Id);
|
||
|
||
IctMss_ResetState();
|
||
|
||
// Guard: chặn pipeline pick H1 FVG có touch trước thời điểm này.
|
||
// Phải đợi touch mới (FVG mới hoặc re-touch) thì mới setup MSS + entry mới.
|
||
g_ictMssAfterCloseGuard = TimeCurrent();
|
||
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"Đóng %s (net %.2f) | M5 FVG #%I64u → Used | chờ touch FVG mới",
|
||
rt, netProfit, m5Id);
|
||
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] Close (%s, net=%.2f) | H1 #%I64u | M5 #%I64u → Used | guard=%s | reset → WAIT_FVG_TOUCH",
|
||
rt, netProfit, h1Id, m5Id,
|
||
TimeToString(g_ictMssAfterCloseGuard, TIME_DATE | TIME_MINUTES));
|
||
}
|
||
|
||
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)
|
||
{
|
||
static bool s_hadPosition = false;
|
||
const bool nowPosition = IctMssEntry_HasOpenPositionMagic(sym);
|
||
|
||
if(s_hadPosition && !nowPosition && g_ictLowTf.mss.phase != ICT_MSS_IDLE)
|
||
{
|
||
// Fallback nếu OnTradeTransaction miss (reload, disconnect, …)
|
||
IctMss_OnPositionClosed(sym, -1L, 0.0);
|
||
}
|
||
s_hadPosition = nowPosition;
|
||
|
||
IctMssEntry_CheckEodCancel(sym);
|
||
IctMssEntry_CheckPendingTimeout(sym);
|
||
IctMssEntry_CheckTpReachedBeforeFill(sym);
|
||
IctMssEntry_CheckStaleLimit(sym);
|
||
IctMssEntry_CheckBreakevenAtRR(sym);
|
||
IctMssEntry_CheckPartialClose(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_CHOCH)
|
||
IctMss_JournalEntryBlock("InpMssTradeEnabled=false");
|
||
return;
|
||
}
|
||
|
||
// Cho phép entry từ CHOCH trở lên (không bắt buộc M5 FVG):
|
||
// ComputeLevels sẽ chọn entry giữa M5 FVG (nếu có) và MSS keyLV (luôn có sau lock).
|
||
//
|
||
// Intraday gate: CHỈ chặn khi InpMssRequireIntradayAligned=true.
|
||
// Mặc định (false) ⇒ entry chỉ dựa vào Daily Bias, không quan tâm Intraday trend
|
||
// (tránh miss setup khi trend chuyển hướng muộn).
|
||
const bool intradayBlock = (InpMssRequireIntradayAligned && !g_ictIntraday.isAllowTrade);
|
||
|
||
if(intradayBlock ||
|
||
g_ictLowTf.mss.phase < ICT_MSS_CHOCH ||
|
||
!g_ictLowTf.mss.chochLocked ||
|
||
g_ictLowTf.mss.chochKeyLevel <= 0.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_H1_TOUCH)
|
||
{
|
||
if(intradayBlock)
|
||
IctMss_JournalEntryBlock("Intraday ngược Bias (InpMssRequireIntradayAligned=true)");
|
||
else if(g_ictLowTf.mss.phase < ICT_MSS_CHOCH)
|
||
IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason);
|
||
else
|
||
IctMss_JournalEntryBlock("Chờ MSS lock (chochKeyLevel)");
|
||
}
|
||
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;
|
||
}
|
||
|
||
// M5 FVG là OPTIONAL: nếu có thì compare với MSS keyLV để chọn entry SL gần hơn
|
||
IctFvgZone m5;
|
||
m5.side = ICT_FVG_NONE;
|
||
if(g_ictLowTf.mss.m5FvgId > 0)
|
||
{
|
||
const int mIdx = IctConfirmFvg_FindById(g_ictLowTf.mss.m5FvgId);
|
||
if(mIdx >= 0)
|
||
m5 = g_ictConfirmFvgZones[mIdx];
|
||
}
|
||
|
||
double entry = 0.0, sl = 0.0, tp = 0.0;
|
||
string lvlReason = "";
|
||
if(!IctMssEntry_ComputeLevels(sym, m5, entry, sl, tp, lvlReason))
|
||
{
|
||
g_ictLowTf.mss.displayReason = StringFormat("%s | %s",
|
||
IctMssPhaseText(g_ictLowTf.mss.phase),
|
||
lvlReason);
|
||
IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason);
|
||
|
||
// Nếu fail vì "Entry xa giá" + bật cancel-stale ⇒ reset setup ngay
|
||
// (tránh stuck CHOCH với entry không bao giờ khớp; chờ POI/touch mới)
|
||
if(InpMssCancelStaleLimit && StringFind(lvlReason, "Entry xa giá") == 0)
|
||
{
|
||
const ulong h1Id = g_ictLowTf.mss.h1FvgId;
|
||
const ulong m5Id = g_ictLowTf.mss.m5FvgId;
|
||
if(g_ictLowTf.mss.pendingTicket > 0)
|
||
{
|
||
IctMssEntry_CancelTicket(g_ictLowTf.mss.pendingTicket);
|
||
g_ictLowTf.mss.pendingTicket = 0;
|
||
}
|
||
IctMssEntry_MarkM5FvgUsed(sym, m5Id);
|
||
IctMss_ResetState();
|
||
g_ictMssAfterCloseGuard = TimeCurrent();
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"%s | %s — reset, chờ POI/touch mới", IctMssPhaseText(ICT_MSS_IDLE), lvlReason);
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] Skip entry (too far): %s | H1 #%I64u | M5 #%I64u → Used | reset",
|
||
lvlReason, h1Id, m5Id);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const bool isBuy = (g_ictLowTf.mss.chochBias == ICT_BIAS_BULL);
|
||
const string entrySource = lvlReason; // ComputeLevels trả entrySource qua reasonOut
|
||
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 | %s",
|
||
entry, sl, tp, rr, entrySource);
|
||
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.pendingPlacedTime = TimeCurrent();
|
||
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 | TP %.2f RR%.1f | %s",
|
||
entry, sl, tp, rr, entrySource);
|
||
}
|
||
|
||
#endif
|