1219 lines
42 KiB
Plaintext
1219 lines
42 KiB
Plaintext
//+------------------------------------------------------------------+
|
||
//| MssSetup.mqh — MSS pipeline (setup phase, không đặt order) |
|
||
//+------------------------------------------------------------------+
|
||
//| Flow: |
|
||
//| IDLE |
|
||
//| │ IctMss_SelectNearestH1Poi() ⇒ chọn H1 FVG gần giá nhất |
|
||
//| │ IctMss_HasFreshFvgTouch() ⇒ touch hợp lệ |
|
||
//| ↓ |
|
||
//| H1_TOUCH |
|
||
//| │ IctMss_UpdateLiveM5Swings() ⇒ liveL0/H0/L1/H1 structural |
|
||
//| │ (v1.171: keylv lock) |
|
||
//| │ IctMss_TryLockMss() ⇒ break body M5 qua L0/H0 |
|
||
//| │ veto MSS nếu InpMssRequireIntradayAligned && ngược bias |
|
||
//| ↓ |
|
||
//| CHOCH (locked) |
|
||
//| │ IctConfirmFvg_ScanNew() ⇒ tìm M5 FVG sau chochTime |
|
||
//| │ IctMss_ZonesOverlapH1() ⇒ M5 FVG gần H1 FVG |
|
||
//| ↓ |
|
||
//| M5_FVG → ENTRY_FILL → READY (chờ giá hồi vào M5 FVG) |
|
||
//| |
|
||
//| FVG fresh touch (v1.182) — cutoff bias-aware: |
|
||
//| cutoff = max(g_ictMssAfterCloseGuard, |
|
||
//| g_ictBiasInto{Bull,Bear}Time) |
|
||
//| Touch hợp lệ phải xảy ra SAU cutoff. |
|
||
//| |
|
||
//| Structural keylv (v1.171) — không phải "pivot mới nhất": |
|
||
//| BEAR (pullback UP): |
|
||
//| liveH0 = đỉnh CAO NHẤT sau touch (extreme) |
|
||
//| liveL0 = đáy gần nhất trước liveH0 (keylv MSS↓) |
|
||
//| liveH1 = đỉnh trước liveL0 |
|
||
//| BULL (pullback DOWN): |
|
||
//| liveL0 = đáy THẤP NHẤT sau touch (extreme) |
|
||
//| liveH0 = đỉnh gần nhất trước liveL0 (keylv MSS↑) |
|
||
//| liveL1 = đáy trước liveH0 |
|
||
//| ⇒ "lock" tự nhiên: pivot SAU extreme không đổi keylv |
|
||
//| |
|
||
//| MSS lock yêu cầu (v1.165): |
|
||
//| - cấu trúc đủ 2 đỉnh/đáy SAU touch |
|
||
//| - body M5 phá liveL0 (BEAR) / liveH0 (BULL) |
|
||
//| |
|
||
//| Globals owned: |
|
||
//| g_ictMssAfterCloseGuard — TS sau khi 1 lệnh đóng (TP/SL/timeout/|
|
||
//| stale limit): pipeline phải đợi touch fresh sau TS này |
|
||
//| |
|
||
//| Public API (chính): |
|
||
//| void IctMss_Update(sym) |
|
||
//| bool IctMss_HasFreshFvgTouch(sym, &h1Zone) |
|
||
//| datetime IctMss_GetFvgTouchTime(sym, &h1Zone) |
|
||
//| void IctMss_ResetState() |
|
||
//| bool IctMss_TryLockMss(sym, tf, h1, bias) |
|
||
//| int IctMss_CountH1PoiEligible() |
|
||
//+------------------------------------------------------------------+
|
||
#ifndef ICT2026_MSSSETUP_MQH
|
||
#define ICT2026_MSSSETUP_MQH
|
||
|
||
#include <ICT2026/ConfirmFvg.mqh>
|
||
#include <ICT2026/DailyBias.mqh>
|
||
#include <ICT2026/Journal.mqh>
|
||
#include <ICT2026/LowTfApi.mqh>
|
||
#include <ICT2026/StructureCore.mqh>
|
||
#include <ICT2026/Swing.mqh>
|
||
|
||
// Guard: sau khi 1 lệnh đóng (TP/SL/manual), pipeline KHÔNG được pick H1 FVG
|
||
// nào có firstTouch trước thời điểm này. Phải đợi 1 touch mới (FVG mới hoặc
|
||
// FVG cũ được re-touched) thì mới tiếp tục setup MSS + entry.
|
||
datetime g_ictMssAfterCloseGuard = 0;
|
||
|
||
string IctMssPhaseText(const ENUM_ICT_MSS_PHASE ph)
|
||
{
|
||
switch(ph)
|
||
{
|
||
case ICT_MSS_H1_TOUCH: return "Retest FVG H1 OK";
|
||
case ICT_MSS_CHOCH: return "MSS OK";
|
||
case ICT_MSS_M5_FVG: return "M5 FVG";
|
||
case ICT_MSS_ENTRY_FILL: return "M5 fill entry";
|
||
case ICT_MSS_READY: return "READY entry";
|
||
default: return "Idle";
|
||
}
|
||
}
|
||
|
||
int IctMss_FindH1FvgById(const ulong id)
|
||
{
|
||
for(int i = 0; i < g_ictFvgCount; i++)
|
||
if(g_ictFvgZones[i].id == id)
|
||
return i;
|
||
return -1;
|
||
}
|
||
|
||
// POI H1: FVG chưa Used, gần giá nhất (không phải Premium cao nhất / Discount thấp nhất)
|
||
double IctMss_DistancePriceToFvg(const double price, const IctFvgZone &z)
|
||
{
|
||
if(price >= z.lower - _Point && price <= z.upper + _Point)
|
||
return 0.0;
|
||
if(price > z.upper)
|
||
return price - z.upper;
|
||
return z.lower - price;
|
||
}
|
||
|
||
bool IctMss_IsH1PoiEligible(const IctFvgZone &z)
|
||
{
|
||
if(!IctFvg_MatchesBias(z))
|
||
return false;
|
||
if(!IctFvg_IsEntryRepPd(z))
|
||
return false;
|
||
if(z.state == ICT_FVG_USED)
|
||
return false;
|
||
return true;
|
||
}
|
||
|
||
int IctMss_CountH1PoiEligible()
|
||
{
|
||
int n = 0;
|
||
for(int i = 0; i < g_ictFvgCount; i++)
|
||
if(IctMss_IsH1PoiEligible(g_ictFvgZones[i]))
|
||
n++;
|
||
return n;
|
||
}
|
||
|
||
int IctMss_SelectNearestH1Poi(const string sym)
|
||
{
|
||
const double px = SymbolInfoDouble(sym, SYMBOL_BID);
|
||
int bestIdx = -1;
|
||
double bestDist = DBL_MAX;
|
||
|
||
for(int i = 0; i < g_ictFvgCount; i++)
|
||
{
|
||
if(!IctMss_IsH1PoiEligible(g_ictFvgZones[i]))
|
||
continue;
|
||
|
||
const double d = IctMss_DistancePriceToFvg(px, g_ictFvgZones[i]);
|
||
if(d < bestDist - _Point)
|
||
{
|
||
bestDist = d;
|
||
bestIdx = i;
|
||
}
|
||
else if(MathAbs(d - bestDist) <= _Point && bestIdx >= 0)
|
||
{
|
||
if(g_ictFvgZones[i].createdTime > g_ictFvgZones[bestIdx].createdTime)
|
||
bestIdx = i;
|
||
}
|
||
}
|
||
return bestIdx;
|
||
}
|
||
|
||
bool IctMss_PriceInsideFvg(const IctFvgZone &h1, const double price)
|
||
{
|
||
return (price >= h1.lower - _Point && price <= h1.upper + _Point);
|
||
}
|
||
|
||
bool IctMss_BarOverlapsFvg(const IctFvgZone &h1, const double barHi, const double barLo)
|
||
{
|
||
return (barHi >= h1.lower - _Point && barLo <= h1.upper + _Point);
|
||
}
|
||
|
||
bool IctMss_LivePriceTouchesFvg(const string sym, const IctFvgZone &h1)
|
||
{
|
||
const double bid = SymbolInfoDouble(sym, SYMBOL_BID);
|
||
const double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
|
||
return IctMss_PriceInsideFvg(h1, bid) || IctMss_PriceInsideFvg(h1, ask);
|
||
}
|
||
|
||
// Touch M5 đầu tiên sau `since` (0 = bất kỳ)
|
||
datetime IctMss_FirstM5TouchInFvg(const string sym, const IctFvgZone &h1, const datetime since = 0)
|
||
{
|
||
const ENUM_TIMEFRAMES tf = InpConfirmTf;
|
||
const int lim = MathMin(InpMssConfirmLookback, iBars(sym, tf) - 2);
|
||
const datetime minT = (since > h1.createdTime) ? since : h1.createdTime;
|
||
datetime best = 0;
|
||
for(int sh = lim; sh >= 1; sh--)
|
||
{
|
||
const datetime t = iTime(sym, tf, sh);
|
||
if(t <= minT)
|
||
continue;
|
||
if(!IctMss_BarOverlapsFvg(h1, iHigh(sym, tf, sh), iLow(sym, tf, sh)))
|
||
continue;
|
||
if(best == 0 || t < best)
|
||
best = t;
|
||
}
|
||
return best;
|
||
}
|
||
|
||
// Cutoff bias-aware: touch chỉ hợp lệ khi xảy ra SAU thời điểm Bias chuyển vào
|
||
// side hiện tại VÀ sau thời điểm lệnh trước đóng.
|
||
datetime IctMss_GetFvgTouchCutoff()
|
||
{
|
||
datetime cutoff = 0;
|
||
if(g_ictDailyBias.bias == ICT_BIAS_BULL && g_ictBiasIntoBullTime > 0)
|
||
cutoff = g_ictBiasIntoBullTime;
|
||
else if(g_ictDailyBias.bias == ICT_BIAS_BEAR && g_ictBiasIntoBearTime > 0)
|
||
cutoff = g_ictBiasIntoBearTime;
|
||
if(g_ictMssAfterCloseGuard > cutoff)
|
||
cutoff = g_ictMssAfterCloseGuard;
|
||
return cutoff;
|
||
}
|
||
|
||
datetime IctMss_GetFvgTouchTime(const string sym, IctFvgZone &h1)
|
||
{
|
||
IctFvg_UpdateZoneState(sym, InpFvgTf, h1);
|
||
const datetime cutoff = IctMss_GetFvgTouchCutoff();
|
||
|
||
// firstTouchTime chỉ hợp lệ nếu xảy ra sau cutoff
|
||
datetime t = 0;
|
||
if(h1.firstTouchTime > 0 && h1.firstTouchTime >= cutoff)
|
||
t = h1.firstTouchTime;
|
||
|
||
// Quét M5 bars để tìm touch fresh sau cutoff (bao gồm cả re-touch sau bias flip)
|
||
const datetime tM5 = IctMss_FirstM5TouchInFvg(sym, h1, cutoff);
|
||
if(tM5 > 0 && (t <= 0 || tM5 < t))
|
||
t = tM5;
|
||
|
||
if(t <= 0 && IctMss_LivePriceTouchesFvg(sym, h1))
|
||
{
|
||
const datetime curBar = iTime(sym, InpConfirmTf, 0);
|
||
if(curBar >= cutoff)
|
||
t = curBar;
|
||
}
|
||
|
||
return t;
|
||
}
|
||
|
||
// Retest = giá chạm FVG (H1/M5/bid-ask). Râu H1 xuyên FVG vẫn OK; chỉ hủy khi H1 body đóng xuyên.
|
||
bool IctMss_HasFvgPriceTouch(const string sym, IctFvgZone &h1)
|
||
{
|
||
return (IctMss_GetFvgTouchTime(sym, h1) > 0);
|
||
}
|
||
|
||
// Touch hợp lệ cho POI mới — cutoff đã được apply trong `IctMss_GetFvgTouchTime`:
|
||
// - touch xảy ra SAU lệnh trước đóng (g_ictMssAfterCloseGuard)
|
||
// - touch xảy ra SAU Bias chuyển vào side hiện tại (g_ictBiasInto*Time)
|
||
// - đạt FILL threshold theo PD position (Pure 25% / Mixed 50%)
|
||
bool IctMss_HasFreshFvgTouch(const string sym, IctFvgZone &h1)
|
||
{
|
||
const datetime tTouch = IctMss_GetFvgTouchTime(sym, h1);
|
||
if(tTouch <= 0)
|
||
return false;
|
||
|
||
const double requiredPct = IctFvg_RequiredTouchFillPct(h1);
|
||
if(requiredPct >= 100.0 - 1e-6)
|
||
return false;
|
||
if(h1.maxFillRatio * 100.0 < requiredPct - 1e-9)
|
||
return false;
|
||
return true;
|
||
}
|
||
|
||
bool IctMss_HasH1FvgRetest(const string sym, IctFvgZone &h1)
|
||
{
|
||
return IctMss_HasFvgPriceTouch(sym, h1);
|
||
}
|
||
|
||
double IctMss_H1Atr(const string sym)
|
||
{
|
||
const int h = iATR(sym, InpFvgTf, InpFvgAtrPeriod);
|
||
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 IctMss_H1ZoneBuffer(const IctFvgZone &h1, const string sym)
|
||
{
|
||
const double gapH = h1.upper - h1.lower;
|
||
double buf = (gapH > _Point && InpMssMaxDistGapPct > 0.0) ?
|
||
gapH * InpMssMaxDistGapPct / 100.0 : 0.0;
|
||
const double atr = IctMss_H1Atr(sym);
|
||
if(atr > 0.0 && InpMssMaxDistAtrMult > 0.0)
|
||
buf = MathMax(buf, atr * InpMssMaxDistAtrMult);
|
||
if(buf <= 0.0)
|
||
buf = MathMax(gapH * 0.5, 20.0 * _Point);
|
||
return buf;
|
||
}
|
||
|
||
void IctMss_GetH1ProximityBounds(const IctFvgZone &h1, const string sym,
|
||
const ENUM_ICT_BIAS bias,
|
||
double &outLo, double &outHi)
|
||
{
|
||
const double gapH = h1.upper - h1.lower;
|
||
const double buf = IctMss_H1ZoneBuffer(h1, sym);
|
||
outLo = h1.lower - buf;
|
||
outHi = h1.upper + buf;
|
||
|
||
if(gapH <= _Point)
|
||
return;
|
||
|
||
if(bias == ICT_BIAS_BEAR && InpMssExtraBelowGapPct > 0.0)
|
||
outLo -= gapH * InpMssExtraBelowGapPct / 100.0;
|
||
if(bias == ICT_BIAS_BULL && InpMssExtraAboveGapPct > 0.0)
|
||
outHi += gapH * InpMssExtraAboveGapPct / 100.0;
|
||
}
|
||
|
||
bool IctMss_PriceNearH1Fvg(const IctFvgZone &h1, const string sym,
|
||
const double price, const ENUM_ICT_BIAS bias)
|
||
{
|
||
if(price <= 0.0 || h1.upper <= h1.lower)
|
||
return false;
|
||
double lo = 0.0, hi = 0.0;
|
||
IctMss_GetH1ProximityBounds(h1, sym, bias, lo, hi);
|
||
return (price >= lo - _Point && price <= hi + _Point);
|
||
}
|
||
|
||
bool IctMss_ChochWithinTouchWindow(const IctFvgZone &h1, const datetime chochTime)
|
||
{
|
||
const datetime tTouch = (h1.firstTouchTime > 0) ? h1.firstTouchTime : h1.createdTime;
|
||
if(chochTime > 0 && chochTime < tTouch)
|
||
return false;
|
||
|
||
if(InpMssMaxM5BarsAfterTouch > 0 && chochTime > 0)
|
||
{
|
||
const int sec = (int)PeriodSeconds(InpConfirmTf);
|
||
const datetime tMax = tTouch + (datetime)(InpMssMaxM5BarsAfterTouch * sec);
|
||
if(chochTime > tMax)
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
int IctMss_MaxSwingShift(const IctSwingSet &sw)
|
||
{
|
||
int m = 1;
|
||
if(sw.hasH0)
|
||
m = MathMax(m, sw.h0.shift);
|
||
if(sw.hasL0)
|
||
m = MathMax(m, sw.l0.shift);
|
||
if(sw.hasH1)
|
||
m = MathMax(m, sw.h1.shift);
|
||
if(sw.hasL1)
|
||
m = MathMax(m, sw.l1.shift);
|
||
return m + 2;
|
||
}
|
||
|
||
bool IctMss_BodyBrokeLevelSince(const string sym, const ENUM_TIMEFRAMES tf,
|
||
const int maxShift, const double level,
|
||
const bool wantBelow)
|
||
{
|
||
const int lim = MathMax(1, MathMin(maxShift, InpMssConfirmLookback));
|
||
for(int sh = 1; sh <= lim; sh++)
|
||
{
|
||
if(wantBelow && IctBodyBreakBelow(sym, tf, sh, level))
|
||
return true;
|
||
if(!wantBelow && IctBodyBreakAbove(sym, tf, sh, level))
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool IctMss_HasM5BarInH1Fvg(const string sym, const IctFvgZone &h1,
|
||
const datetime since)
|
||
{
|
||
const ENUM_TIMEFRAMES tf = InpConfirmTf;
|
||
const int lim = MathMin(InpMssConfirmLookback, iBars(sym, tf) - 2);
|
||
for(int sh = 1; sh <= lim; sh++)
|
||
{
|
||
if(since > 0 && iTime(sym, tf, sh) < since)
|
||
break;
|
||
if(IctMss_BarOverlapsFvg(h1, iHigh(sym, tf, sh), iLow(sym, tf, sh)))
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Structural keylv — KHÔNG dùng "pivot mới nhất" (nhảy mỗi nến).
|
||
// Định nghĩa: keylv = pivot GẦN NHẤT TRƯỚC extreme của pullback ⇒
|
||
// tự nhiên "lock" tới khi pullback mở rộng (BEAR: tạo high mới cao hơn;
|
||
// BULL: tạo low mới thấp hơn).
|
||
//
|
||
// BEAR bias (pullback UP, mục tiêu MSS↓ phá L0):
|
||
// liveH0 = ĐỈNH CAO NHẤT sau tTouch (extreme đỉnh pullback)
|
||
// liveL0 = đáy gần nhất theo time TRƯỚC liveH0 (keylv — "đáy tạo ra đỉnh cao nhất")
|
||
// liveH1 = đỉnh gần nhất TRƯỚC liveL0 (đỉnh cũ — đủ cấu trúc 2 đỉnh)
|
||
// liveL1 = đáy gần nhất TRƯỚC liveH1 (đáy cũ — đủ cấu trúc 2 đáy)
|
||
// SL = max(H0, H1) + buffer
|
||
//
|
||
// BULL bias (pullback DOWN, mục tiêu MSS↑ phá H0):
|
||
// liveL0 = ĐÁY THẤP NHẤT sau tTouch (extreme đáy pullback)
|
||
// liveH0 = đỉnh gần nhất theo time TRƯỚC liveL0 (keylv — "đỉnh tạo ra đáy thấp nhất")
|
||
// liveL1 = đáy gần nhất TRƯỚC liveH0 (đáy cũ — đủ cấu trúc 2 đáy)
|
||
// liveH1 = đỉnh gần nhất TRƯỚC liveL1 (đỉnh cũ — đủ cấu trúc 2 đỉnh)
|
||
// SL = min(L0, L1) − buffer
|
||
//
|
||
// Nhờ tham chiếu "TRƯỚC extreme":
|
||
// - Đỉnh mới tạo SAU extreme low (bullish bias) ⇒ KHÔNG ảnh hưởng keylv
|
||
// - Đáy mới tạo SAU extreme high (bearish bias) ⇒ KHÔNG ảnh hưởng keylv
|
||
// ⇒ keylv chỉ "nhảy" khi pullback thực sự mở rộng tạo extreme mới
|
||
bool IctMss_UpdateLiveM5Swings(const string sym, const ENUM_ICT_BIAS bias,
|
||
const datetime tTouch)
|
||
{
|
||
g_ictLowTf.mss.liveL0Price = 0.0;
|
||
g_ictLowTf.mss.liveH0Price = 0.0;
|
||
g_ictLowTf.mss.liveL0Time = 0;
|
||
g_ictLowTf.mss.liveH0Time = 0;
|
||
g_ictLowTf.mss.liveL1Price = 0.0;
|
||
g_ictLowTf.mss.liveH1Price = 0.0;
|
||
g_ictLowTf.mss.liveL1Time = 0;
|
||
g_ictLowTf.mss.liveH1Time = 0;
|
||
|
||
if(bias != ICT_BIAS_BEAR && bias != ICT_BIAS_BULL)
|
||
return false;
|
||
|
||
// BẮT BUỘC có tTouch hợp lệ — nếu không pipeline sẽ lấy cả pivot trước touch
|
||
// (filter `time < tTouch` không có tác dụng khi tTouch = 0).
|
||
if(tTouch <= 0)
|
||
return false;
|
||
|
||
IctSwingPoint highs[], lows[];
|
||
IctCollectSwings(sym, InpConfirmTf, InpConfirmSwingRange,
|
||
InpConfirmSwingLookback, highs, lows);
|
||
const int nH = ArraySize(highs);
|
||
const int nL = ArraySize(lows);
|
||
if(nH == 0 || nL == 0)
|
||
return false;
|
||
|
||
if(bias == ICT_BIAS_BEAR)
|
||
{
|
||
// 1) liveH0 = đỉnh cao nhất sau tTouch (extreme của pullback up)
|
||
int idxH0 = -1;
|
||
double maxHigh = -DBL_MAX;
|
||
for(int i = 0; i < nH; i++)
|
||
{
|
||
if(highs[i].time < tTouch) continue;
|
||
if(highs[i].price > maxHigh)
|
||
{
|
||
maxHigh = highs[i].price;
|
||
idxH0 = i;
|
||
}
|
||
}
|
||
if(idxH0 < 0) return false;
|
||
g_ictLowTf.mss.liveH0Price = highs[idxH0].price;
|
||
g_ictLowTf.mss.liveH0Time = highs[idxH0].time;
|
||
const datetime tH0 = highs[idxH0].time;
|
||
|
||
// 2) liveL0 = đáy GẦN NHẤT THEO TIME TRƯỚC liveH0 (keylv MSS↓)
|
||
int idxL0 = -1;
|
||
datetime tL0Best = 0;
|
||
for(int i = 0; i < nL; i++)
|
||
{
|
||
if(lows[i].time < tTouch) continue;
|
||
if(lows[i].time >= tH0) continue;
|
||
if(lows[i].time > tL0Best)
|
||
{
|
||
tL0Best = lows[i].time;
|
||
idxL0 = i;
|
||
}
|
||
}
|
||
if(idxL0 < 0) return false;
|
||
g_ictLowTf.mss.liveL0Price = lows[idxL0].price;
|
||
g_ictLowTf.mss.liveL0Time = lows[idxL0].time;
|
||
const datetime tL0 = lows[idxL0].time;
|
||
|
||
// 3) liveH1 = đỉnh GẦN NHẤT THEO TIME TRƯỚC liveL0
|
||
int idxH1 = -1;
|
||
datetime tH1Best = 0;
|
||
for(int i = 0; i < nH; i++)
|
||
{
|
||
if(highs[i].time < tTouch) continue;
|
||
if(highs[i].time >= tL0) continue;
|
||
if(highs[i].time > tH1Best)
|
||
{
|
||
tH1Best = highs[i].time;
|
||
idxH1 = i;
|
||
}
|
||
}
|
||
if(idxH1 >= 0)
|
||
{
|
||
g_ictLowTf.mss.liveH1Price = highs[idxH1].price;
|
||
g_ictLowTf.mss.liveH1Time = highs[idxH1].time;
|
||
}
|
||
|
||
// 4) liveL1 = đáy GẦN NHẤT THEO TIME TRƯỚC liveH1 (nếu có)
|
||
if(idxH1 >= 0)
|
||
{
|
||
int idxL1 = -1;
|
||
datetime tL1Best = 0;
|
||
for(int i = 0; i < nL; i++)
|
||
{
|
||
if(lows[i].time < tTouch) continue;
|
||
if(lows[i].time >= tH1Best) continue;
|
||
if(lows[i].time > tL1Best)
|
||
{
|
||
tL1Best = lows[i].time;
|
||
idxL1 = i;
|
||
}
|
||
}
|
||
if(idxL1 >= 0)
|
||
{
|
||
g_ictLowTf.mss.liveL1Price = lows[idxL1].price;
|
||
g_ictLowTf.mss.liveL1Time = lows[idxL1].time;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// BULL bias (pullback DOWN)
|
||
// 1) liveL0 = đáy thấp nhất sau tTouch (extreme của pullback down)
|
||
int idxL0 = -1;
|
||
double minLow = DBL_MAX;
|
||
for(int i = 0; i < nL; i++)
|
||
{
|
||
if(lows[i].time < tTouch) continue;
|
||
if(lows[i].price < minLow)
|
||
{
|
||
minLow = lows[i].price;
|
||
idxL0 = i;
|
||
}
|
||
}
|
||
if(idxL0 < 0) return false;
|
||
g_ictLowTf.mss.liveL0Price = lows[idxL0].price;
|
||
g_ictLowTf.mss.liveL0Time = lows[idxL0].time;
|
||
const datetime tL0 = lows[idxL0].time;
|
||
|
||
// 2) liveH0 = đỉnh GẦN NHẤT THEO TIME TRƯỚC liveL0 (keylv MSS↑)
|
||
int idxH0 = -1;
|
||
datetime tH0Best = 0;
|
||
for(int i = 0; i < nH; i++)
|
||
{
|
||
if(highs[i].time < tTouch) continue;
|
||
if(highs[i].time >= tL0) continue;
|
||
if(highs[i].time > tH0Best)
|
||
{
|
||
tH0Best = highs[i].time;
|
||
idxH0 = i;
|
||
}
|
||
}
|
||
if(idxH0 < 0) return false;
|
||
g_ictLowTf.mss.liveH0Price = highs[idxH0].price;
|
||
g_ictLowTf.mss.liveH0Time = highs[idxH0].time;
|
||
const datetime tH0 = highs[idxH0].time;
|
||
|
||
// 3) liveL1 = đáy GẦN NHẤT THEO TIME TRƯỚC liveH0
|
||
int idxL1 = -1;
|
||
datetime tL1Best = 0;
|
||
for(int i = 0; i < nL; i++)
|
||
{
|
||
if(lows[i].time < tTouch) continue;
|
||
if(lows[i].time >= tH0) continue;
|
||
if(lows[i].time > tL1Best)
|
||
{
|
||
tL1Best = lows[i].time;
|
||
idxL1 = i;
|
||
}
|
||
}
|
||
if(idxL1 >= 0)
|
||
{
|
||
g_ictLowTf.mss.liveL1Price = lows[idxL1].price;
|
||
g_ictLowTf.mss.liveL1Time = lows[idxL1].time;
|
||
}
|
||
|
||
// 4) liveH1 = đỉnh GẦN NHẤT THEO TIME TRƯỚC liveL1 (nếu có)
|
||
if(idxL1 >= 0)
|
||
{
|
||
int idxH1 = -1;
|
||
datetime tH1Best = 0;
|
||
for(int i = 0; i < nH; i++)
|
||
{
|
||
if(highs[i].time < tTouch) continue;
|
||
if(highs[i].time >= tL1Best) continue;
|
||
if(highs[i].time > tH1Best)
|
||
{
|
||
tH1Best = highs[i].time;
|
||
idxH1 = i;
|
||
}
|
||
}
|
||
if(idxH1 >= 0)
|
||
{
|
||
g_ictLowTf.mss.liveH1Price = highs[idxH1].price;
|
||
g_ictLowTf.mss.liveH1Time = highs[idxH1].time;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// Hủy setup: H1 đóng (shift=1) — thân xuyên FVG (không giữ giá). Râu xuyên vẫn tiếp tục MSS.
|
||
// v1.186: dùng IctBodyBreakBuffer (N × spread) thay vì _Point — siết để
|
||
// một close-through "sát mép" do nhiễu không làm invalidate setup.
|
||
bool IctMss_H1BodyInvalidatedSetup(const string sym, const IctFvgZone &h1,
|
||
const ENUM_ICT_BIAS bias)
|
||
{
|
||
if(h1.upper <= h1.lower)
|
||
return false;
|
||
|
||
const double cls = iClose(sym, InpFvgTf, 1);
|
||
const double buf = IctBodyBreakBuffer(sym);
|
||
if(bias == ICT_BIAS_BEAR)
|
||
return (cls > h1.upper + buf);
|
||
if(bias == ICT_BIAS_BULL)
|
||
return (cls < h1.lower - buf);
|
||
return false;
|
||
}
|
||
|
||
bool IctMss_CheckH1InvalidationOnNewBar(const string sym, const IctFvgZone &h1,
|
||
const ENUM_ICT_BIAS bias)
|
||
{
|
||
const datetime h1Bar1 = iTime(sym, InpFvgTf, 1);
|
||
if(h1Bar1 <= 0)
|
||
return false;
|
||
|
||
if(h1Bar1 == g_ictLowTf.mss.h1LastInvalidBarTime)
|
||
return false;
|
||
|
||
g_ictLowTf.mss.h1LastInvalidBarTime = h1Bar1;
|
||
return IctMss_H1BodyInvalidatedSetup(sym, h1, bias);
|
||
}
|
||
|
||
bool IctMss_FindMssBreakBar(const string sym, const ENUM_TIMEFRAMES tf,
|
||
const double keyLevel, const datetime keyTime,
|
||
const bool wantBreakBelow,
|
||
datetime &breakTimeOut)
|
||
{
|
||
breakTimeOut = 0;
|
||
if(keyLevel <= 0.0 || keyTime <= 0)
|
||
return false;
|
||
|
||
const int lim = MathMin(InpMssConfirmLookback, iBars(sym, tf) - 2);
|
||
for(int sh = 1; sh <= lim; sh++)
|
||
{
|
||
if(iTime(sym, tf, sh) < keyTime)
|
||
break;
|
||
const bool broke = wantBreakBelow ?
|
||
IctBodyBreakBelow(sym, tf, sh, keyLevel) :
|
||
IctBodyBreakAbove(sym, tf, sh, keyLevel);
|
||
if(!broke)
|
||
continue;
|
||
breakTimeOut = iTime(sym, tf, sh);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool IctMss_TryLockMss(const string sym, const ENUM_TIMEFRAMES tf,
|
||
const IctFvgZone &h1, const ENUM_ICT_BIAS bias)
|
||
{
|
||
if(g_ictLowTf.mss.chochLocked && g_ictLowTf.mss.chochKeyLevel > 0.0)
|
||
return true;
|
||
|
||
const datetime tTouch = (g_ictLowTf.mss.h1TouchTime > 0) ?
|
||
g_ictLowTf.mss.h1TouchTime : h1.firstTouchTime;
|
||
if(tTouch <= 0)
|
||
return false;
|
||
|
||
if(!IctMss_UpdateLiveM5Swings(sym, bias, tTouch))
|
||
return false;
|
||
|
||
datetime breakT = 0;
|
||
|
||
if(bias == ICT_BIAS_BEAR)
|
||
{
|
||
// Pullback bullish phải có CẤU TRÚC ĐẦY ĐỦ: 2 đỉnh (H0 & H1) đều SAU touch
|
||
// → tránh case mới có 1 đỉnh từ pullback mới, H1 còn dính sóng cũ
|
||
if(g_ictLowTf.mss.liveH1Price <= 0.0 ||
|
||
g_ictLowTf.mss.liveH0Time < tTouch ||
|
||
g_ictLowTf.mss.liveH1Time < tTouch)
|
||
return false;
|
||
|
||
if(!IctMss_FindMssBreakBar(sym, tf, g_ictLowTf.mss.liveL0Price,
|
||
g_ictLowTf.mss.liveL0Time, true, breakT))
|
||
return false;
|
||
if(breakT < tTouch)
|
||
return false;
|
||
|
||
// SL trên cao nhất giữa H0 và H1 (cap toàn pullback bull) + buffer (cộng ở MssEntry)
|
||
double slSwingHi = g_ictLowTf.mss.liveH0Price;
|
||
if(g_ictLowTf.mss.liveH1Price > slSwingHi)
|
||
slSwingHi = g_ictLowTf.mss.liveH1Price;
|
||
|
||
g_ictLowTf.mss.chochKeyLevel = g_ictLowTf.mss.liveL0Price;
|
||
g_ictLowTf.mss.chochKeyTime = g_ictLowTf.mss.liveL0Time;
|
||
g_ictLowTf.mss.slSwingPrice = slSwingHi;
|
||
}
|
||
else if(bias == ICT_BIAS_BULL)
|
||
{
|
||
// Pullback bearish phải có CẤU TRÚC ĐẦY ĐỦ: 2 đáy (L0 & L1) đều SAU touch
|
||
// → tránh case mới có 1 đáy từ pullback mới, L1 còn dính sóng cũ
|
||
if(g_ictLowTf.mss.liveL1Price <= 0.0 ||
|
||
g_ictLowTf.mss.liveL0Time < tTouch ||
|
||
g_ictLowTf.mss.liveL1Time < tTouch)
|
||
return false;
|
||
|
||
if(!IctMss_FindMssBreakBar(sym, tf, g_ictLowTf.mss.liveH0Price,
|
||
g_ictLowTf.mss.liveH0Time, false, breakT))
|
||
return false;
|
||
if(breakT < tTouch)
|
||
return false;
|
||
|
||
// SL dưới thấp nhất giữa L0 và L1 (đáy toàn pullback bear) + buffer (trừ ở MssEntry)
|
||
double slSwingLo = g_ictLowTf.mss.liveL0Price;
|
||
if(g_ictLowTf.mss.liveL1Price > 0.0 && g_ictLowTf.mss.liveL1Price < slSwingLo)
|
||
slSwingLo = g_ictLowTf.mss.liveL1Price;
|
||
|
||
g_ictLowTf.mss.chochKeyLevel = g_ictLowTf.mss.liveH0Price;
|
||
g_ictLowTf.mss.chochKeyTime = g_ictLowTf.mss.liveH0Time;
|
||
g_ictLowTf.mss.slSwingPrice = slSwingLo;
|
||
}
|
||
else
|
||
return false;
|
||
|
||
g_ictLowTf.mss.chochTime = breakT;
|
||
g_ictLowTf.mss.chochLocked = true;
|
||
g_ictLowTf.mss.chochBias = bias;
|
||
return true;
|
||
}
|
||
|
||
bool IctMss_ZonesOverlapH1(const IctFvgZone &h1, const string sym,
|
||
const ENUM_ICT_BIAS bias,
|
||
const double zoneLo, const double zoneHi)
|
||
{
|
||
if(zoneHi <= zoneLo)
|
||
return false;
|
||
double hLo = 0.0, hHi = 0.0;
|
||
IctMss_GetH1ProximityBounds(h1, sym, bias, hLo, hHi);
|
||
return !(zoneHi < hLo - _Point || zoneLo > hHi + _Point);
|
||
}
|
||
|
||
bool IctMss_IsChochNearH1Fvg(const string sym, const IctFvgZone &h1,
|
||
const ENUM_ICT_BIAS bias,
|
||
const double keyLevel, const double anchorPrice,
|
||
const datetime chochTime)
|
||
{
|
||
const bool nearAnchor = (anchorPrice > 0.0 &&
|
||
IctMss_PriceNearH1Fvg(h1, sym, anchorPrice, bias));
|
||
const bool nearKey = (keyLevel > 0.0 &&
|
||
IctMss_PriceNearH1Fvg(h1, sym, keyLevel, bias));
|
||
|
||
if(!nearAnchor && !nearKey)
|
||
return false;
|
||
|
||
return IctMss_ChochWithinTouchWindow(h1, chochTime);
|
||
}
|
||
|
||
bool IctMss_GetConfirmMssSwing(const string sym, const ENUM_ICT_BIAS bias, double &swingOut)
|
||
{
|
||
swingOut = 0.0;
|
||
|
||
IctSwingSet sw;
|
||
ENUM_ICT_STRUCT structural = ICT_STRUCT_NONE;
|
||
if(IctBuildConfirmSwingSet(sym, sw, structural))
|
||
{
|
||
if(bias == ICT_BIAS_BEAR && sw.hasH0)
|
||
{
|
||
// BEAR SL: max(H0, H1) — cap toàn pullback bull
|
||
double hi = sw.h0.price;
|
||
if(sw.hasH1 && sw.h1.price > hi)
|
||
hi = sw.h1.price;
|
||
swingOut = hi;
|
||
return true;
|
||
}
|
||
if(bias == ICT_BIAS_BULL && sw.hasL0)
|
||
{
|
||
// BULL SL: min(L0, L1) — đáy toàn pullback bear
|
||
double lo = sw.l0.price;
|
||
if(sw.hasL1 && sw.l1.price < lo)
|
||
lo = sw.l1.price;
|
||
swingOut = lo;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
IctSwingPoint highs[], lows[];
|
||
IctCollectSwings(sym, InpConfirmTf, InpConfirmSwingRange, InpConfirmSwingLookback,
|
||
highs, lows);
|
||
const int nH = ArraySize(highs);
|
||
const int nL = ArraySize(lows);
|
||
if(bias == ICT_BIAS_BEAR && nH > 0)
|
||
{
|
||
// max của 2 swing high gần nhất
|
||
double hi = highs[nH - 1].price;
|
||
if(nH >= 2 && highs[nH - 2].price > hi)
|
||
hi = highs[nH - 2].price;
|
||
swingOut = hi;
|
||
return true;
|
||
}
|
||
if(bias == ICT_BIAS_BULL && nL > 0)
|
||
{
|
||
// min của 2 swing low gần nhất
|
||
double lo = lows[nL - 1].price;
|
||
if(nL >= 2 && lows[nL - 2].price < lo)
|
||
lo = lows[nL - 2].price;
|
||
swingOut = lo;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool IctMss_FindExtremePivot(const IctSwingPoint &pts[], const int n,
|
||
const int maxShift, const bool wantHighest,
|
||
IctSwingPoint &out)
|
||
{
|
||
out.Clear();
|
||
int best = -1;
|
||
for(int i = 0; i < n; i++)
|
||
{
|
||
if(pts[i].shift > maxShift || pts[i].shift < 3)
|
||
continue;
|
||
if(best < 0)
|
||
best = i;
|
||
else if(wantHighest)
|
||
{
|
||
if(pts[i].price > pts[best].price)
|
||
best = i;
|
||
}
|
||
else
|
||
{
|
||
if(pts[i].price < pts[best].price)
|
||
best = i;
|
||
}
|
||
}
|
||
if(best < 0)
|
||
return false;
|
||
out = pts[best];
|
||
return true;
|
||
}
|
||
|
||
bool IctMss_DetectChoch(const string sym, const ENUM_TIMEFRAMES tf,
|
||
const ENUM_ICT_BIAS bias,
|
||
double &keyLevelOut, datetime &keyTimeOut,
|
||
double &slSwingOut)
|
||
{
|
||
keyLevelOut = 0.0;
|
||
keyTimeOut = 0;
|
||
slSwingOut = 0.0;
|
||
|
||
IctSwingPoint highs[], lows[];
|
||
IctCollectSwings(sym, tf, InpConfirmSwingRange, InpConfirmSwingLookback,
|
||
highs, lows);
|
||
const int nH = ArraySize(highs);
|
||
const int nL = ArraySize(lows);
|
||
const int maxSh = InpMssConfirmLookback;
|
||
|
||
if(bias == ICT_BIAS_BULL)
|
||
{
|
||
IctSwingPoint extremeLo;
|
||
if(!IctMss_FindExtremePivot(lows, nL, maxSh, false, extremeLo))
|
||
return false;
|
||
|
||
IctSwingPoint keyHi = IctFindMostRecentOlder(highs, nH, extremeLo.shift);
|
||
if(!keyHi.Valid())
|
||
return false;
|
||
|
||
keyLevelOut = keyHi.price;
|
||
keyTimeOut = keyHi.time;
|
||
slSwingOut = extremeLo.price;
|
||
return IctBodyBreakAbove(sym, tf, 1, keyLevelOut);
|
||
}
|
||
|
||
if(bias == ICT_BIAS_BEAR)
|
||
{
|
||
IctSwingPoint extremeHi;
|
||
if(!IctMss_FindExtremePivot(highs, nH, maxSh, true, extremeHi))
|
||
return false;
|
||
|
||
IctSwingPoint keyLo = IctFindMostRecentOlder(lows, nL, extremeHi.shift);
|
||
if(!keyLo.Valid())
|
||
return false;
|
||
|
||
keyLevelOut = keyLo.price;
|
||
keyTimeOut = keyLo.time;
|
||
slSwingOut = extremeHi.price;
|
||
return IctBodyBreakBelow(sym, tf, 1, keyLevelOut);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
bool IctMss_PriceInEntryZone(const string sym, const ENUM_TIMEFRAMES tf,
|
||
const IctFvgZone &zone)
|
||
{
|
||
if(!IctFvg_HasFillAtLeast(zone, InpMssEntryFillPct))
|
||
return false;
|
||
|
||
const double fillPx = IctFvg_PriceAtFillPct(zone, InpMssEntryFillPct);
|
||
const double bid = SymbolInfoDouble(sym, SYMBOL_BID);
|
||
const double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
|
||
const double mid = (bid + ask) * 0.5;
|
||
|
||
if(zone.side == ICT_FVG_BULL)
|
||
return (mid <= fillPx + _Point && mid >= zone.lower - _Point);
|
||
if(zone.side == ICT_FVG_BEAR)
|
||
return (mid >= fillPx - _Point && mid <= zone.upper + _Point);
|
||
return false;
|
||
}
|
||
|
||
void IctMss_MarkFvgUsed(IctFvgZone &zone, const string sym)
|
||
{
|
||
if(zone.id == 0)
|
||
return;
|
||
|
||
zone.state = ICT_FVG_USED;
|
||
if(zone.fvgUsedTime == 0)
|
||
zone.fvgUsedTime = iTime(sym, InpFvgTf, 0);
|
||
zone.timeEnd = IctFvg_GetFvgDrawTimeEnd(sym, InpFvgTf, zone);
|
||
}
|
||
|
||
void IctMss_MarkFvgUsedOnMssSuccess(const string sym, const ulong fvgId)
|
||
{
|
||
if(fvgId == 0)
|
||
return;
|
||
|
||
const int idx = IctMss_FindH1FvgById(fvgId);
|
||
if(idx < 0)
|
||
return;
|
||
|
||
const bool fresh = (g_ictFvgZones[idx].state != ICT_FVG_USED);
|
||
if(fresh)
|
||
IctMss_MarkFvgUsed(g_ictFvgZones[idx], sym);
|
||
|
||
if(g_ictLowTf.mss.h1WatchFvgId == fvgId)
|
||
g_ictLowTf.mss.h1WatchFvgId = 0;
|
||
|
||
if(fresh && InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] FVG #%I64u → Used (giữ giá + MSS M5 OK)", fvgId);
|
||
}
|
||
|
||
void IctMss_FailFvgH1Body(const string sym, const ulong fvgId, const string reason)
|
||
{
|
||
if(fvgId > 0)
|
||
{
|
||
const int idx = IctMss_FindH1FvgById(fvgId);
|
||
if(idx >= 0)
|
||
IctMss_MarkFvgUsed(g_ictFvgZones[idx], sym);
|
||
}
|
||
|
||
IctMss_ResetState();
|
||
g_ictLowTf.mss.displayReason = reason;
|
||
|
||
if(InpMssLogJournal)
|
||
PrintFormat("[ICT2026/MSS] FVG #%I64u → Used (H1 body xuyên) | %s", fvgId, reason);
|
||
g_ictMssJournalLast = "";
|
||
}
|
||
|
||
void IctMss_ResetPipelineKeepWatch()
|
||
{
|
||
const ulong watch = g_ictLowTf.mss.h1WatchFvgId;
|
||
const datetime inv = g_ictLowTf.mss.h1LastInvalidBarTime;
|
||
g_ictLowTf.mss.Clear();
|
||
g_ictLowTf.mss.h1WatchFvgId = watch;
|
||
g_ictLowTf.mss.h1LastInvalidBarTime = inv;
|
||
IctMss_JournalReset();
|
||
}
|
||
|
||
void IctMss_CheckH1WatchInvalidation(const string sym)
|
||
{
|
||
const ulong watchId = g_ictLowTf.mss.h1WatchFvgId;
|
||
if(watchId == 0)
|
||
return;
|
||
|
||
const int idx = IctMss_FindH1FvgById(watchId);
|
||
if(idx < 0)
|
||
{
|
||
g_ictLowTf.mss.h1WatchFvgId = 0;
|
||
return;
|
||
}
|
||
|
||
if(g_ictFvgZones[idx].state == ICT_FVG_USED)
|
||
{
|
||
g_ictLowTf.mss.h1WatchFvgId = 0;
|
||
return;
|
||
}
|
||
|
||
const ENUM_ICT_BIAS bias = g_ictDailyBias.bias;
|
||
if(bias != ICT_BIAS_BEAR && bias != ICT_BIAS_BULL)
|
||
return;
|
||
|
||
if(!IctMss_CheckH1InvalidationOnNewBar(sym, g_ictFvgZones[idx], bias))
|
||
return;
|
||
|
||
IctMss_FailFvgH1Body(sym, watchId,
|
||
(bias == ICT_BIAS_BEAR) ?
|
||
"FVG Used: H1 đóng thân trên Premium — không giữ giá" :
|
||
"FVG Used: H1 đóng thân dưới Discount — không giữ giá");
|
||
}
|
||
|
||
void IctMss_ResetState()
|
||
{
|
||
g_ictLowTf.mss.Clear();
|
||
IctMss_JournalReset();
|
||
}
|
||
|
||
void IctMss_Update(const string sym)
|
||
{
|
||
g_ictLowTf.mss.displayReason = IctMssPhaseText(g_ictLowTf.mss.phase);
|
||
|
||
IctMss_CheckH1WatchInvalidation(sym);
|
||
|
||
// Pipeline gate = Bias rõ ràng (KHÔNG gate theo Intraday trend).
|
||
// → POI scan + MSS detect chạy liên tục trong giai đoạn intraday transition
|
||
// → Entry vẫn bị chặn bởi isAllowTrade trong IctMssEntry_Update
|
||
const ENUM_TIMEFRAMES cTf = InpConfirmTf;
|
||
const ENUM_ICT_FVG_SIDE wantSide = IctFvgSideFromBias(g_ictDailyBias.bias);
|
||
if(wantSide == ICT_FVG_NONE)
|
||
{
|
||
if(g_ictLowTf.mss.phase != ICT_MSS_IDLE)
|
||
{
|
||
if(g_ictLowTf.mss.h1WatchFvgId == 0 && g_ictLowTf.mss.h1FvgId > 0)
|
||
g_ictLowTf.mss.h1WatchFvgId = g_ictLowTf.mss.h1FvgId;
|
||
IctMss_ResetPipelineKeepWatch();
|
||
}
|
||
g_ictLowTf.mss.displayReason = "Bias none — MSS pipeline tạm dừng";
|
||
return;
|
||
}
|
||
|
||
if(g_ictLowTf.mss.phase == ICT_MSS_IDLE)
|
||
{
|
||
const int hIdx = IctMss_SelectNearestH1Poi(sym);
|
||
if(hIdx < 0)
|
||
{
|
||
g_ictLowTf.mss.h1FvgId = 0;
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"Chờ H1 %s FVG gần giá (%d POI / %d FVG)",
|
||
(wantSide == ICT_FVG_BEAR) ? "Premium" : "Discount",
|
||
IctMss_CountH1PoiEligible(), g_ictFvgCount);
|
||
return;
|
||
}
|
||
|
||
g_ictLowTf.mss.h1FvgId = g_ictFvgZones[hIdx].id;
|
||
|
||
if(!IctMss_HasFreshFvgTouch(sym, g_ictFvgZones[hIdx]))
|
||
{
|
||
const double px = SymbolInfoDouble(sym, SYMBOL_BID);
|
||
const double dist = IctMss_DistancePriceToFvg(px, g_ictFvgZones[hIdx]);
|
||
const datetime tTouch = IctMss_GetFvgTouchTime(sym, g_ictFvgZones[hIdx]);
|
||
const bool staleTouch = (tTouch > 0 && g_ictMssAfterCloseGuard > 0 &&
|
||
tTouch < g_ictMssAfterCloseGuard);
|
||
const double reqPct = IctFvg_RequiredTouchFillPct(g_ictFvgZones[hIdx]);
|
||
const double curPct = g_ictFvgZones[hIdx].maxFillRatio * 100.0;
|
||
if(staleTouch)
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"POI #%I64u [%.0f–%.0f] — chờ touch mới (sau lệnh trước)",
|
||
g_ictFvgZones[hIdx].id,
|
||
g_ictFvgZones[hIdx].lower, g_ictFvgZones[hIdx].upper);
|
||
else if(tTouch > 0 && curPct < reqPct)
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"POI #%I64u [%.0f–%.0f] fill %.0f%% / cần %.0f%% — chờ vào sâu hơn",
|
||
g_ictFvgZones[hIdx].id,
|
||
g_ictFvgZones[hIdx].lower, g_ictFvgZones[hIdx].upper,
|
||
curPct, reqPct);
|
||
else
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"POI #%I64u [%.0f–%.0f] dist %.0f pts — chờ chạm",
|
||
g_ictFvgZones[hIdx].id,
|
||
g_ictFvgZones[hIdx].lower, g_ictFvgZones[hIdx].upper,
|
||
dist / _Point);
|
||
return;
|
||
}
|
||
|
||
const datetime tTouch = IctMss_GetFvgTouchTime(sym, g_ictFvgZones[hIdx]);
|
||
g_ictLowTf.mss.phase = ICT_MSS_H1_TOUCH;
|
||
g_ictLowTf.mss.h1FvgId = g_ictFvgZones[hIdx].id;
|
||
g_ictLowTf.mss.h1WatchFvgId = g_ictFvgZones[hIdx].id;
|
||
g_ictLowTf.mss.h1TouchTime = tTouch;
|
||
g_ictLowTf.mss.h1LastInvalidBarTime = 0;
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"Giá đã chạm FVG — theo dõi MSS M5 [%.0f–%.0f]",
|
||
g_ictFvgZones[hIdx].lower, g_ictFvgZones[hIdx].upper);
|
||
}
|
||
|
||
const int h1Idx = IctMss_FindH1FvgById(g_ictLowTf.mss.h1FvgId);
|
||
if(h1Idx < 0)
|
||
{
|
||
IctMss_ResetState();
|
||
g_ictLowTf.mss.displayReason = "H1 FVG mất";
|
||
return;
|
||
}
|
||
|
||
if(g_ictFvgZones[h1Idx].state == ICT_FVG_USED &&
|
||
g_ictLowTf.mss.phase <= ICT_MSS_H1_TOUCH &&
|
||
!g_ictLowTf.mss.chochLocked)
|
||
{
|
||
IctMss_ResetState();
|
||
g_ictLowTf.mss.displayReason = "FVG POI Used — chọn FVG gần giá tiếp theo";
|
||
return;
|
||
}
|
||
|
||
if(g_ictLowTf.mss.phase >= ICT_MSS_H1_TOUCH && g_ictLowTf.mss.h1TouchTime <= 0)
|
||
{
|
||
const datetime t = IctMss_GetFvgTouchTime(sym, g_ictFvgZones[h1Idx]);
|
||
if(t > 0)
|
||
g_ictLowTf.mss.h1TouchTime = t;
|
||
}
|
||
|
||
if(g_ictLowTf.mss.phase == ICT_MSS_H1_TOUCH && !g_ictLowTf.mss.chochLocked)
|
||
{
|
||
const datetime tTouch = g_ictLowTf.mss.h1TouchTime;
|
||
if(IctMss_UpdateLiveM5Swings(sym, g_ictDailyBias.bias, tTouch))
|
||
{
|
||
// Check cấu trúc pullback đầy đủ (2 đỉnh/đáy đều sau touch)
|
||
const bool fullStruct = (g_ictDailyBias.bias == ICT_BIAS_BEAR) ?
|
||
(g_ictLowTf.mss.liveH1Price > 0.0 &&
|
||
g_ictLowTf.mss.liveH0Time >= tTouch &&
|
||
g_ictLowTf.mss.liveH1Time >= tTouch) :
|
||
(g_ictLowTf.mss.liveL1Price > 0.0 &&
|
||
g_ictLowTf.mss.liveL0Time >= tTouch &&
|
||
g_ictLowTf.mss.liveL1Time >= tTouch);
|
||
|
||
if(!fullStruct)
|
||
{
|
||
g_ictLowTf.mss.displayReason = (g_ictDailyBias.bias == ICT_BIAS_BEAR) ?
|
||
"Chờ pullback đủ cấu trúc (2 đỉnh H0&H1 sau touch)" :
|
||
"Chờ pullback đủ cấu trúc (2 đáy L0&L1 sau touch)";
|
||
}
|
||
else if(g_ictDailyBias.bias == ICT_BIAS_BEAR)
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"Chờ MSS↓ phá L0=%.2f | H0=%.2f H1=%.2f (M5 bull)",
|
||
g_ictLowTf.mss.liveL0Price,
|
||
g_ictLowTf.mss.liveH0Price,
|
||
g_ictLowTf.mss.liveH1Price);
|
||
else
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"Chờ MSS↑ phá H0=%.2f | L0=%.2f L1=%.2f (M5 bear)",
|
||
g_ictLowTf.mss.liveH0Price,
|
||
g_ictLowTf.mss.liveL0Price,
|
||
g_ictLowTf.mss.liveL1Price);
|
||
}
|
||
else
|
||
g_ictLowTf.mss.displayReason = "M5 trong FVG — chờ pivot L0/H0";
|
||
|
||
if(!IctMss_TryLockMss(sym, cTf, g_ictFvgZones[h1Idx], g_ictDailyBias.bias))
|
||
return;
|
||
|
||
// Veto MSS nếu Intraday NGƯỢC Bias — CHỈ áp dụng khi yêu cầu align (mặc định OFF).
|
||
// Mặc định: cho phép MSS dù Intraday ngược ⇒ entry sớm hơn khi trend đổi muộn,
|
||
// giảm trường hợp "giá chạy tới TP rồi mới khớp limit → quay lại SL".
|
||
if(InpMssRequireIntradayAligned &&
|
||
!IctIntraday_TrendAlignsWithBias(g_ictDailyBias.bias, g_ictIntraday.trend))
|
||
{
|
||
IctMss_MarkFvgUsedOnMssSuccess(sym, g_ictLowTf.mss.h1FvgId);
|
||
const string sideLbl = (g_ictDailyBias.bias == ICT_BIAS_BEAR) ? "MSS↓" : "MSS↑";
|
||
const string trendLbl = IctTrendDisplayShort(g_ictIntraday.trend);
|
||
IctMss_ResetState();
|
||
g_ictLowTf.mss.displayReason = StringFormat(
|
||
"%s khớp khi Intraday=%s ngược Bias — bỏ POI, chờ POI mới",
|
||
sideLbl, trendLbl);
|
||
return;
|
||
}
|
||
|
||
IctMss_MarkFvgUsedOnMssSuccess(sym, g_ictLowTf.mss.h1FvgId);
|
||
|
||
const double keyLv = g_ictLowTf.mss.chochKeyLevel;
|
||
const double slSwing = g_ictLowTf.mss.slSwingPrice;
|
||
|
||
g_ictLowTf.mss.phase = ICT_MSS_CHOCH;
|
||
IctConfirmFvg_ScanNew(sym, cTf, wantSide, g_ictLowTf.mss.chochTime, true);
|
||
g_ictLowTf.mss.displayReason = (g_ictDailyBias.bias == ICT_BIAS_BEAR) ?
|
||
StringFormat("MSS↓ @ L0=%.2f | SL H0=%.2f — quét M5 FVG", keyLv, slSwing) :
|
||
StringFormat("MSS↑ @ H0=%.2f | SL L0=%.2f — quét M5 FVG", keyLv, slSwing);
|
||
}
|
||
|
||
if(g_ictLowTf.mss.phase == ICT_MSS_CHOCH)
|
||
{
|
||
IctConfirmFvg_ScanNew(sym, cTf, wantSide, g_ictLowTf.mss.chochTime, false);
|
||
|
||
const int mIdx = IctConfirmFvg_FindLatestAfter(g_ictLowTf.mss.chochTime, wantSide);
|
||
if(mIdx < 0)
|
||
{
|
||
g_ictLowTf.mss.displayReason = "MSS OK — chờ M5 FVG";
|
||
return;
|
||
}
|
||
|
||
if(!IctMss_ZonesOverlapH1(g_ictFvgZones[h1Idx], sym, g_ictDailyBias.bias,
|
||
g_ictConfirmFvgZones[mIdx].lower,
|
||
g_ictConfirmFvgZones[mIdx].upper))
|
||
{
|
||
g_ictLowTf.mss.displayReason = "M5 FVG xa H1 FVG — chờ FVG gần hơn";
|
||
return;
|
||
}
|
||
|
||
g_ictLowTf.mss.phase = ICT_MSS_M5_FVG;
|
||
g_ictLowTf.mss.m5FvgId = g_ictConfirmFvgZones[mIdx].id;
|
||
g_ictLowTf.mss.m5FvgTime = g_ictConfirmFvgZones[mIdx].createdTime;
|
||
g_ictLowTf.mss.displayReason = StringFormat("M5 FVG #%I64u [%.2f–%.2f]",
|
||
g_ictLowTf.mss.m5FvgId,
|
||
g_ictConfirmFvgZones[mIdx].lower,
|
||
g_ictConfirmFvgZones[mIdx].upper);
|
||
}
|
||
|
||
if(g_ictLowTf.mss.phase >= ICT_MSS_M5_FVG && g_ictLowTf.mss.m5FvgId > 0)
|
||
{
|
||
const int mIdx = IctConfirmFvg_FindById(g_ictLowTf.mss.m5FvgId);
|
||
if(mIdx < 0)
|
||
{
|
||
g_ictLowTf.mss.phase = ICT_MSS_CHOCH;
|
||
g_ictLowTf.mss.m5FvgId = 0;
|
||
g_ictLowTf.mss.displayReason = "M5 FVG mất — quét lại";
|
||
return;
|
||
}
|
||
|
||
IctFvg_UpdateZoneState(sym, cTf, g_ictConfirmFvgZones[mIdx]);
|
||
|
||
if(IctMss_PriceInEntryZone(sym, cTf, g_ictConfirmFvgZones[mIdx]))
|
||
{
|
||
g_ictLowTf.mss.phase = ICT_MSS_READY;
|
||
g_ictLowTf.mss.displayReason = StringFormat("READY | M5 fill %.0f%% | %s %s",
|
||
g_ictConfirmFvgZones[mIdx].maxFillRatio * 100.0,
|
||
IctFvgSideText(wantSide),
|
||
IctPdZoneText(IctFvg_GetRepPd(g_ictFvgZones[h1Idx])));
|
||
}
|
||
else if(IctFvg_HasFillAtLeast(g_ictConfirmFvgZones[mIdx], InpMssEntryFillPct))
|
||
{
|
||
g_ictLowTf.mss.phase = ICT_MSS_ENTRY_FILL;
|
||
g_ictLowTf.mss.displayReason = StringFormat("M5 chạm %.0f%% — canh entry",
|
||
InpMssEntryFillPct);
|
||
}
|
||
else
|
||
{
|
||
g_ictLowTf.mss.phase = ICT_MSS_M5_FVG;
|
||
g_ictLowTf.mss.displayReason = StringFormat("Chờ hồi M5 FVG %.0f%% (fill %.0f%%)",
|
||
InpMssEntryFillPct,
|
||
g_ictConfirmFvgZones[mIdx].maxFillRatio * 100.0);
|
||
}
|
||
}
|
||
|
||
IctMss_JournalPipeline(g_ictLowTf.mss.displayReason);
|
||
}
|
||
|
||
#endif
|