پرش به محتوا
کریپکس

ICT · اندیکاتور

Crypex ICT Models: ساختار بازار، FVG، بلوک‌ها و Unicorn (تحلیلی)

اندیکاتور تحلیلی ساختار و نواحی سبک ICT برای TradingView، متاتریدر ۵ و متاتریدر ۴: MSS و BOS، Fair Value Gap با CE و IFVG، Order Block، Breaker، Mitigation، Unicorn، پریمیوم/دیسکانت و OTE، تشخیص Raid و Run و پنل وضعیت. پیش‌فرض فقط تحلیلی است و ستاپ خرید و فروش نمی‌سازد؛ مدل‌های ICT 2022، Silver Bullet، Unicorn و OTE اختیاری‌اند.

پیش‌نمایش

اندیکاتور Crypex ICT Models روی طلا با MSS، Raid، FVG، Breaker، Unicorn و OTE در Preset Clean
طلا ۱۵ دقیقه‌ای با Preset «Clean»: MSS و Raid روی PDH و EQH، Unicorn (هم‌پوشانی Breaker و FVG)، Breaker نارنجی، FVGهای محدود، ناحیه‌ی OTE و پنل وضعیت. حالت پیش‌فرض تحلیلی است و ستاپ ندارد.رندر از خروجی کدطلا (آتی GC=F)، ۱۵ دقیقه، اکتبر ۲۰۲۶
اندیکاتور Crypex ICT Models روی طلا با Order Block، Unicorn، MSS و Raid
روز دیگر روی طلا: Raid روی PDH و PDL و EQL، MSS با ⚡ دیسپلیسمنت، Unicorn، Order Block نزولی، FVG سالم و OTE.رندر از خروجی کدطلا (آتی GC=F)، ۱۵ دقیقه، اکتبر ۲۰۲۶

این تصویرها اسکرین‌شات از TradingView یا متاتریدر نیستند؛ با اجرای همین کد روی داده‌ی واقعی بازار ساخته و رسم شده‌اند. ظاهر دقیق در پلتفرم شما (فونت، رنگ‌ها و اندازه) کمی متفاوت است.

ict-models-crypex.pine · Pine Script v6 · ۱٬۵۲۳ خط
// Crypex ICT Models — اندیکاتور ساختار، ناحیه‌ها و مدل‌های ورود ICT (MSS، FVG، OB، Breaker، Unicorn، OTE، ICT 2022، Silver Bullet)
// © Crypex.org — All rights reserved. | حق نشر محفوظ برای Crypex.org
// License: personal, non-commercial use only. Redistribution or resale without written permission is prohibited.
// مجوز: استفاده شخصی و غیرتجاری. بازنشر یا فروش مجدد بدون اجازه‌ی کتبی ممنوع است.
// Educational tool, not investment advice. | ابزار آموزشی است، توصیه‌ی سرمایه‌گذاری نیست.

//@version=6
// Crypex ICT Models (2 of 2) — structure, zones and entry models:
//  STRUCTURE Swings, Displacement, MSS / BOS, Premium / Discount / Equilibrium, OTE 62-70.5-79.
//  ZONES     FVG (BISI/SIBI, CE, partial fill), IFVG, Volume Imbalance, BPR, Order Block, Breaker, Mitigation, Rejection, Propulsion,
//            Vacuum, NWOG/NDOG, Unicorn (Breaker + FVG).
//  MODELS    ICT 2022 (raid -> MSS -> FVG), Silver Bullet, Unicorn, OTE — entry / SL / TP, confluence score 0-6, win-rate table.
// It keeps its own light liquidity levels (PDH/PDL, PWH/PWL, session highs/lows, EQH/EQL) for raids and targets;
// the full session/liquidity drawing lives in "Crypex ICT Time". All state changes happen on CONFIRMED bars only (no repaint of signals).
// Session times are New York time by default. Best on intraday charts 1m–1H.
indicator('Crypex ICT Models', 'ICT Models', overlay = true, max_bars_back = 1500, max_labels_count = 500, max_lines_count = 500, max_boxes_count = 500)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// CONSTANTS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BRAND_GREEN = #0BA31C
BRAND_DARK  = #1E2229
C_BULL      = #089981
C_BEAR      = #F23645
C_BLUE      = #2962FF
C_ORANGE    = #FF9800
C_PURPLE    = #9C27B0
C_YELLOW    = #FFD600
C_GRAY      = #787B86
C_CYAN      = #00BCD4
C_PINK      = #E91E63
C_LIME      = #8BC34A

G1  = '1 · General & time zones'
G2  = '2 · Killzone times (used by the models)'
G3  = '3 · Silver Bullet windows'
G6  = '4 · Liquidity levels (raids & targets)'
G8  = '5 · Market structure & displacement'
G9  = '6 · Premium / Discount & OTE'
G10 = '7 · Fair Value Gaps (FVG / IFVG / VI / BPR)'
G11 = '8 · Blocks, Breaker, Mitigation, Rejection, Propulsion, Vacuum, Gaps, Unicorn'
G13 = '9 · Models & signals'
G14 = '10 · Panel & watermark'

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// INPUTS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
preset    = input.string('Clean (recommended)', 'Preset', options = ['Clean (recommended)', 'Standard', 'Full', 'Custom'], group = G1, tooltip = 'Clean = فقط ارزشمندترین اجزای ICT با چارت خلوت (پیش‌فرض). Standard = همه‌ی اجزا مثل نسخه‌ی معمول. Full = همه‌چیز، بیشترین تعداد. Custom = فقط تنظیمات دستی خودتان (همان گزینه‌های پایین). در سه حالت اول، گزینه‌های زیر فقط روی «جزئیات نمایش» اثر دارند: نمایش علامت دیسپلیسمنت، BOS، IFVG، VI، BPR، Mitigation، Rejection، Propulsion، Vacuum، خط 50٪ بلوک‌ها، ناحیه‌ی پریمیوم/دیسکانت، لول‌های سشن، PMH/PML، جدول Win-rate و سقف تعداد FVG/بلوک/لول. بقیه‌ی گزینه‌ها همیشه فعال‌اند.')
tzIn      = 'America/New_York'   // ICT sessions are always defined in New York time (daylight saving handled by TradingView)
tzDisp    = input.string('Asia/Tehran', 'Show times on chart in', options = ['America/New_York', 'Asia/Tehran', 'UTC', 'Europe/London'], group = G1, tooltip = 'ساعت نمایش داده‌شده روی برچسب‌ها و پنل. پیش‌فرض: وقت تهران.')
tzAnc     = 'America/New_York'   // day / week / month boundary and anchors (Midnight Open, 08:30, 09:30)
lblGap    = input.float(3.0, 'Right-side labels: min vertical gap (% of recent range)', minval = 0.5, maxval = 15, step = 0.5, group = G1, tooltip = 'برچسب‌های سمت راست (لول‌ها، بلوک‌ها، FVG، ستاپ‌ها) خودکار از هم فاصله می‌گیرند تا روی هم نیفتند. عدد بزرگ‌تر = فاصله‌ی بیشتر.')
lblPad    = input.int(3, 'Right-side labels: offset from last bar (bars)', minval = 1, maxval = 30, group = G1, tooltip = 'برای دیده‌شدن برچسب‌ها، حاشیه‌ی راست چارت را بازتر کنید (Settings > Scales > Right margin حدود ۱۵ تا ۲۰).')
lblNear   = input.float(0.3, 'Show zone labels within (× recent range from price)', minval = 0.1, maxval = 3.0, step = 0.1, group = G1, tooltip = 'برچسب بلوک‌ها و FVGهای دور از قیمت مخفی می‌شود تا ستون راست شلوغ نشود (خود ناحیه روی چارت می‌ماند).')
lsz       = input.string(size.small, 'Label size', options = [size.tiny, size.small, size.normal, size.large], group = G1)

asiaOn  = input.bool(true, 'Asian range', group = G2, inline = 'as')
asiaR   = input.session('2000-0000', '', group = G2, inline = 'as')
asiaC   = input.color(#9C27B0, '', group = G2, inline = 'as')
lonOn   = input.bool(true, 'London killzone', group = G2, inline = 'lo')
lonR    = input.session('0200-0500', '', group = G2, inline = 'lo')
lonC    = input.color(#2962FF, '', group = G2, inline = 'lo')
nyaOn   = input.bool(true, 'New York AM killzone', group = G2, inline = 'na')
nyaR    = input.session('0700-1000', '', group = G2, inline = 'na')
nyaC    = input.color(#089981, '', group = G2, inline = 'na')
lcOn    = input.bool(true, 'London Close killzone', group = G2, inline = 'lc')
lcR     = input.session('1000-1200', '', group = G2, inline = 'lc')
lcC     = input.color(#FF9800, '', group = G2, inline = 'lc')
nypOn   = input.bool(true, 'New York PM killzone', group = G2, inline = 'np')
nypR    = input.session('1330-1600', '', group = G2, inline = 'np')
nypC    = input.color(#E91E63, '', group = G2, inline = 'np')
sessLvlOn = input.bool(true, 'Turn finished session highs/lows into liquidity levels', group = G2)

sbOn   = input.bool(true, 'Silver Bullet windows', group = G3, inline = 'sb')
sb1R   = input.session('0300-0400', 'SB 1 (London)', group = G3)
sb2R   = input.session('1000-1100', 'SB 2 (NY AM)', group = G3)
sb3R   = input.session('1400-1500', 'SB 3 (NY PM)', group = G3)

pdOn     = input.bool(true, 'PDH / PDL', group = G6, inline = 'l1')
pdC      = input.color(#2962FF, '', group = G6, inline = 'l1')
pwOn     = input.bool(true, 'PWH / PWL', group = G6, inline = 'l2')
pwC      = input.color(#9C27B0, '', group = G6, inline = 'l2')
pmOn     = input.bool(false, 'PMH / PML', group = G6, inline = 'l3')
pmC      = input.color(#E91E63, '', group = G6, inline = 'l3')
sessLC   = input.color(#787B86, 'Session levels colour', group = G6)
eqOn     = input.bool(true, 'EQH / EQL (equal highs / lows)', group = G6, inline = 'l4')
eqC      = input.color(#FF6D00, '', group = G6, inline = 'l4')
eqTol    = input.float(0.10, 'EQ tolerance (× ATR)', minval = 0.01, step = 0.01, group = G6)
raidLbl  = input.bool(true, 'Mark raids (sweep + close back = turtle soup) and runs', group = G6)
pendBars = input.int(3, 'Bars to wait before a break counts as a Run', minval = 1, maxval = 20, group = G6, tooltip = 'مهلت تکلیف شکست سطح نقدینگی بر حسب کندل: اگر در این مدت قیمت به داخل سطح برنگردد، «Run» ثبت می‌شود. روی ۱ دقیقه یعنی ۳ دقیقه و روی ۱۵ دقیقه یعنی ۴۵ دقیقه؛ متناسب با تایم‌فریم تنظیم کنید.')
keepSwept = input.int(60, 'Keep swept levels for (bars)', minval = 0, maxval = 500, group = G6)
maxLiq   = input.int(40, 'Max liquidity levels on chart', minval = 10, maxval = 120, group = G6)

swLen    = input.int(5, 'Swing length', minval = 2, maxval = 30, group = G8)
dispATR  = input.float(1.2, 'Displacement: body ≥ × ATR', minval = 0.3, step = 0.1, group = G8)
dispBody = input.float(0.6, 'Displacement: body / range ≥', minval = 0.3, maxval = 1.0, step = 0.05, group = G8)
dispMark = input.bool(true, 'Mark displacement candles', group = G8)
mssNeedD = input.bool(false, 'MSS needs displacement (else plain break = BOS/CHoCH label)', group = G8)
showBos  = input.bool(true, 'Show BOS', group = G8)
showMss  = input.bool(true, 'Show MSS', group = G8)
structKeep = input.int(14, 'Structure lines to keep', minval = 2, maxval = 60, group = G8)

pdZoneOn = input.bool(true, 'Premium / Discount / Equilibrium (last dealing range)', group = G9)
oteOn    = input.bool(true, 'OTE zone (62 – 79 %)', group = G9)
oteLo    = input.float(0.62, 'OTE from', minval = 0.5, maxval = 0.9, step = 0.005, group = G9)
oteHi    = input.float(0.79, 'OTE to', minval = 0.5, maxval = 0.95, step = 0.005, group = G9)

fvgOn    = input.bool(true, 'Fair Value Gaps', group = G10)
fvgMin   = input.float(0.10, 'Min FVG size (× ATR)', minval = 0, step = 0.05, group = G10)
fvgKeep  = input.int(16, 'Max active FVGs', minval = 2, maxval = 60, group = G10)
fvgQual  = input.bool(false, 'Quality filter: middle candle must be a strong move', group = G10, tooltip = 'ICT: FVG معتبر معمولاً از کندل میانیِ دیسپلیسمنتی (حرکت قوی) ساخته می‌شود، نه هر شکاف کوچک سه‌کندلی. اگر روشن باشد، فقط FVGهایی که بدنه‌ی کندل وسط ≥ مقدار پایین × ATR و هم‌جهت شکاف است ثبت می‌شوند. در Preset «Clean» همیشه روشن است.')
fvgMid   = input.float(0.5, 'Quality filter: middle candle body ≥ × ATR', minval = 0.2, maxval = 3.0, step = 0.1, group = G10)
fvgCE    = input.bool(true, 'Show Consequent Encroachment (50 %)', group = G10)
fvgShrink = input.bool(true, 'Shrink FVG when partly filled', group = G10)
ifvgOn   = input.bool(true, 'Inversion FVG (IFVG)', group = G10)
viOn     = input.bool(true, 'Volume Imbalance (VI)', group = G10)
bprOn    = input.bool(true, 'Balanced Price Range (BPR)', group = G10)
fvgBullC = input.color(#089981, 'Bullish FVG colour', group = G10)
fvgBearC = input.color(#F23645, 'Bearish FVG colour', group = G10)

obOn     = input.bool(true, 'Order Block (BOS continuation)', group = G11)
brkOn    = input.bool(true, 'Breaker Block (approx.: MSS after a lower-low / higher-high sweep)', group = G11)
mitOn    = input.bool(true, 'Mitigation Block (approx.: MSS without sweep)', group = G11)
rbOn     = input.bool(true, 'Rejection Block (long-wick swing candle)', group = G11)
pbOn     = input.bool(true, 'Propulsion Block (candle rejecting a block)', group = G11)
vacOn    = input.bool(true, 'Vacuum Block / NWOG / NDOG (price gaps)', group = G11)
uniOn    = input.bool(true, 'Unicorn (Breaker + FVG overlap)', group = G11)
zoneMode = input.string('Full candle', 'Block zone from', options = ['Full candle', 'Body only', 'Open to extreme'], group = G11)
zoneMT   = input.bool(true, 'Show 50 % mean threshold of blocks', group = G11)
zoneMax  = input.int(24, 'Max blocks on chart', minval = 4, maxval = 80, group = G11)
rbPct    = input.float(0.5, 'Rejection: wick ≥ % of candle', minval = 0.3, maxval = 0.9, step = 0.05, group = G11)
rbATR    = input.float(0.5, 'Rejection: wick ≥ × ATR', minval = 0, step = 0.1, group = G11)
gapATR   = input.float(0.3, 'Gap: min size (× ATR)', minval = 0.05, step = 0.05, group = G11)
obBullC  = input.color(#089981, 'Bullish blocks colour', group = G11)
obBearC  = input.color(#F23645, 'Bearish blocks colour', group = G11)

sigOn    = input.bool(false, 'Trade setups: Entry / SL / TP signals (off = analysis only)', group = G13, tooltip = 'پیش‌فرض خاموش: اندیکاتور فقط تحلیلی است (ساختار، MSS، FVG، بلوک‌ها، نقدینگی، Raid). هیچ ستاپ خرید/فروش، خط ورود/حد ضرر/حد سود، آمار برد و باخت یا هشدار BUY/SELL تولید نمی‌شود. اگر روشن کنید، مدل‌های پایین (ICT 2022، Silver Bullet، Unicorn، OTE) ستاپ می‌سازند. توجه: آمار برد/باخت یک شبیه‌سازی ساده است، نه بک‌تست.')
m22On    = input.bool(true, 'Model: ICT 2022 (raid → MSS → FVG)', group = G13)
mSbOn    = input.bool(true, 'Model: Silver Bullet (FVG inside window)', group = G13)
mUniOn   = input.bool(true, 'Model: Unicorn', group = G13)
mOteOn   = input.bool(true, 'Model: OTE', group = G13)
uniWin   = input.int(6, 'Unicorn: FVG must form within N bars after the MSS', minval = 1, maxval = 30, group = G13, tooltip = 'FVG باید در همان حرکت MSS ساخته شود، نه ساعت‌ها بعد. عدد کوچک‌تر = Unicorn سخت‌گیرانه‌تر.')
kzOnly   = input.bool(true, 'Signals only inside killzones / Silver Bullet', group = G13)
mssWin   = input.int(30, 'ICT 2022: max bars from raid to MSS', minval = 3, maxval = 200, group = G13)
entryMode = input.string('FVG CE (50%)', 'Entry on FVG', options = ['FVG CE (50%)', 'FVG edge'], group = G13)
slBuf    = input.float(0.10, 'Stop buffer (× ATR)', minval = 0, step = 0.05, group = G13)
minRR    = input.float(1.5, 'Min reward : risk', minval = 0.5, step = 0.1, group = G13)
fbRR     = input.float(2.0, 'Fallback target (R) if no liquidity target', minval = 0.5, step = 0.5, group = G13)
expBars  = input.int(40, 'Cancel unfilled setup after (bars)', minval = 3, maxval = 500, group = G13)
minScore = input.int(3, 'Min confluence score (0–6)', minval = 0, maxval = 6, group = G13, tooltip = 'امتیاز: کیل‌زون، رید نقدینگی هم‌جهت، MSS هم‌جهت، دیسپلیسمنت، بایاس HTF، پریمیوم/دیسکانت')
sbBias   = input.bool(true, 'Silver Bullet needs raid or MSS in the same direction', group = G13)
biasTF   = input.timeframe('240', 'HTF bias timeframe', group = G13)
biasLen  = input.int(21, 'HTF bias EMA length', minval = 2, group = G13)
oteSL    = input.string('Beyond OTE zone', 'OTE model stop', options = ['Beyond OTE zone', 'Swing origin (100%)'], group = G13, tooltip = 'Beyond OTE zone = زیر/بالای ۷۹٪ (ریسک کم). Swing origin = پشت ابتدای حرکت (ریسک بیشتر).')
ageMin   = input.int(240, 'Event validity for the score (minutes)', minval = 15, maxval = 2880, group = G13, tooltip = 'اعتبار Raid / MSS برای امتیاز و Silver Bullet بر حسب دقیقه‌ی واقعی (با زمان خود رویداد؛ تعطیلی و گپ بازار هم حساب می‌شود). رویداد قدیمی‌تر از این مدت امتیاز نمی‌گیرد.')
htfReq   = input.bool(false, 'Require HTF bias in the trade direction', group = G13, tooltip = 'پیش‌فرض خاموش: HTF فقط یک امتیاز از ۶ است. اگر روشن شود، ستاپ برخلاف بایاس HTF ساخته نمی‌شود.')
costATR  = input.float(0.03, 'Trading cost (× ATR) in the win-rate table', minval = 0, step = 0.01, group = G13, tooltip = 'اسپرد و لغزش تقریبی: برای رسیدن به TP قیمت باید این مقدار فراتر برود و SL به‌اندازه‌ی آن زودتر فعال می‌شود.')
showSetups = input.bool(true, 'Draw entry / SL / TP', group = G13, tooltip = 'آمار برد/باخت یک شبیه‌سازی ساده روی کندل‌های بسته است، بک‌تست نیست. ترتیب حرکت داخل کندل معلوم نیست، پس سیاست محافظه‌کارانه است: اگر در یک کندل هم SL و هم TP لمس شود = ضرر. اگر TP قبل از ورود (یا هم‌زمان با ورود) لمس شود = ستاپ لغو و بدون نتیجه. ورود OTE یک لیمیت در قیمت Close کندل سیگنال است و از کندل بعد پر می‌شود.')

showPanel = input.bool(true, 'Show info panel', group = G14)
panelPos  = input.string(position.bottom_right, 'Panel position', options = [position.top_left, position.top_center, position.top_right, position.middle_left, position.middle_center, position.middle_right, position.bottom_left, position.bottom_center, position.bottom_right], group = G14, tooltip = 'جای جدول روی چارت. هر کدام از ۹ نقطه را می‌توانید انتخاب کنید.')
psz       = input.string(size.small, 'Panel text size', options = [size.tiny, size.small, size.normal], group = G14)
showStats = input.bool(false, 'Win-rate table of the models (adds 5 rows to the panel)', group = G14)
showWatermark = input.bool(true, 'Show crypex.org watermark', group = G14)
wmPos     = input.string(position.bottom_center, 'Watermark position', options = [position.top_left, position.top_center, position.top_right, position.middle_left, position.middle_center, position.middle_right, position.bottom_left, position.bottom_center, position.bottom_right], group = G14)
wmGap     = input.float(5.0, 'Gap from left (%)', minval = 0, maxval = 30, step = 0.5, group = G14, tooltip = 'Bottom-left only: empty space so the watermark sits beside the TradingView logo.')
wmSizeStr = input.string('large', 'Watermark size', options = ['small', 'normal', 'large', 'huge'], group = G14)
wmTransp  = input.int(0, 'Watermark transparency', minval = 0, maxval = 95, group = G14)


// ── presets: Clean / Standard / Full set the "how much to draw" options; Custom uses the inputs exactly as set
f_pb(bool c, bool st, bool fu, bool cu) => preset == 'Clean (recommended)' ? c : preset == 'Standard' ? st : preset == 'Full' ? fu : cu
f_pi(int c, int st, int fu, int cu) => preset == 'Clean (recommended)' ? c : preset == 'Standard' ? st : preset == 'Full' ? fu : cu
f_pf(float c, float st, float fu, float cu) => preset == 'Clean (recommended)' ? c : preset == 'Standard' ? st : preset == 'Full' ? fu : cu
bool dispMarkE = f_pb(false, true, true, dispMark)
bool showBosE = f_pb(false, true, true, showBos)
bool ifvgOnE = f_pb(false, true, true, ifvgOn)
bool viOnE = f_pb(false, true, true, viOn)
bool bprOnE = f_pb(false, true, true, bprOn)
bool mitOnE = f_pb(false, true, true, mitOn)
bool rbOnE = f_pb(false, true, true, rbOn)
bool pbOnE = f_pb(false, true, true, pbOn)
bool vacOnE = f_pb(false, true, true, vacOn)
bool zoneMTE = f_pb(false, true, true, zoneMT)
bool pdZoneOnE = f_pb(false, true, true, pdZoneOn)
bool sessLvlOnE = f_pb(false, true, true, sessLvlOn)
bool pmOnE = f_pb(false, false, true, pmOn)
bool fvgQualE = f_pb(true, false, false, fvgQual)
float fvgMinE = f_pf(0.3, 0.1, 0.05, fvgMin)
bool showStatsE = sigOn and f_pb(false, false, true, showStats)
int fvgKeepE = f_pi(5, 16, 30, fvgKeep)
int zoneMaxE = f_pi(6, 24, 40, zoneMax)
float lblNearE = f_pf(0.25, 0.3, 0.6, lblNear)
int maxLiqE = f_pi(12, 40, 60, maxLiq)
int keepSweptE = f_pi(20, 60, 100, keepSwept)
int structKeepE = f_pi(6, 14, 24, structKeep)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// TYPES
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
type Sess
    string name
    string rng
    bool   on
    color  col
    bool   proj    = false
    bool   lvl     = false
    bool   kz      = false
    bool   active  = false
    int    sBar    = 0
    int    sTime   = 0
    float  hi      = na
    float  lo      = na
    int    hiBar   = 0
    int    loBar   = 0
    box    bx      = na
    label  lb      = na
    float  fHi     = na
    float  fLo     = na
    int    fHiBar  = 0
    int    fLoBar  = 0
    array<line>  pl
    array<label> pll
type Liq
    float  price
    string name
    int    dir          // +1 buy-side (above price), -1 sell-side (below price)
    int    kind         // 1 session, 2 PD, 3 PW, 4 PM, 5 EQ
    int    bar
    bool   swept   = false
    int    sweptBar = 0
    bool   pend    = false
    int    hitBar  = 0
    float  hitExt  = na
    label  mk      = na
    line   ln
    label  lb
type Fvg
    float  top
    float  bot
    int    dir
    int    bar
    int    state    = 0   // 0 normal, 1 inverted
    float  ext      = na  // extreme of the 3-candle formation (SL reference)
    bool   uni      = false
    bool   bp       = false
    string nm       = 'FVG'
    box    bx
    line   ce
    label  lb
type Zone
    float  top
    float  bot
    int    dir
    int    kind          // 1 OB, 2 Breaker, 3 Mitigation, 4 Rejection, 5 Propulsion, 6 Gap
    string nm = ''
    int    bar
    int    born    = 0
    bool   touched = false
    bool   prop    = false
    bool   uni     = false
    box    bx
    line   mt
    label  lb
type Setup
    int    model
    int    dir
    float  entry
    float  sl
    float  tp
    int    born
    int    fillBar = 0
    int    state   = 0    // 0 pending, 1 active, 2 finished
    int    score
    float  rr
    float  cost    = 0.0
    line   lEn
    line   lSl
    line   lTp
    label  lb
type St
    // structure
    int    trend    = 0
    float  swHi     = na
    int    swHiBar  = 0
    float  pSwHi    = na
    bool   swHiX    = true
    float  swLo     = na
    int    swLoBar  = 0
    float  pSwLo    = na
    bool   swLoX    = true
    int    mssDir   = 0
    int    mssBar   = -9999
    int    mssT     = 0
    int    dispBullBar = -9999
    int    dispBearBar = -9999
    // raids
    int    raidDir  = 0
    int    raidBar  = -9999
    int    raidT    = 0
    float  raidPx   = na
    string raidName = ''
    bool   raidRej  = false
    int    raidSc   = 0
    label  raidMk   = na
    float  raidPen  = 0.0
    int    raidScBar = -1
    int    evRun    = 0
    // day / week / month
    float  curDH    = na
    float  curDL    = na
    int    curDHB   = 0
    int    curDLB   = 0
    float  curWH    = na
    float  curWL    = na
    int    curWHB   = 0
    int    curWLB   = 0
    float  curMH    = na
    float  curML    = na
    int    curMHB   = 0
    int    curMLB   = 0
    bool   jdDone   = false
    // anchors
    float  moPx     = na
    // dealing range
    float  eq       = na
    float  rngTop   = na
    float  rngBot   = na
    int    oteDir   = 0
    float  oteTop   = na
    float  oteBot   = na
    float  oteMid   = na
    float  oteSl    = na
    float  oteTp    = na
    bool   oteFired = false
    // SMT
    float  phM      = na
    float  phS      = na
    float  plM      = na
    float  plS      = na
    int    smtDir   = 0
    int    smtBar   = -9999
    // PO3
    float  po3Hi    = na
    float  po3Lo    = na
    int    po3Bias  = 0
    // model state
    int    w22Dir   = 0
    int    w22Bar   = 0
    float  w22Px    = na
    bool   sbFired  = false
    // events of this bar
    int    evRaid   = 0
    int    evMss    = 0
    int    evBrk    = 0
    int    evFvg    = 0
    int    evUni    = 0
    int    evSig    = 0
    int    evJudas  = 0
    int    evSmt    = 0
    string evModel  = ''
    float  nfTop    = na
    float  nfBot    = na
    float  nfExt    = na
    int    nfDir    = 0
    float  uniTop   = na
    float  uniBot   = na
    float  uniFar   = na
    int    uniDir   = 0
    int    uniBar   = -9999
    bool   uniDone  = true

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// GLOBAL STATE
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var St S = St.new()
var Sess sAsia  = Sess.new(name = 'Asia',      rng = asiaR,  on = asiaOn, col = asiaC,  proj = false,  lvl = true,  kz = false, pl = array.new<line>(), pll = array.new<label>())
var Sess sLon   = Sess.new(name = 'London KZ', rng = lonR,   on = lonOn,   col = lonC,   proj = false, lvl = true, kz = true,  pl = array.new<line>(), pll = array.new<label>())
var Sess sNya   = Sess.new(name = 'NY AM KZ',  rng = nyaR,   on = nyaOn,   col = nyaC,   proj = false, lvl = true, kz = true,  pl = array.new<line>(), pll = array.new<label>())
var Sess sLc    = Sess.new(name = 'London Close KZ', rng = lcR, on = lcOn, col = lcC,   proj = false, lvl = true, kz = true,  pl = array.new<line>(), pll = array.new<label>())
var Sess sNyp   = Sess.new(name = 'NY PM KZ',  rng = nypR,   on = nypOn,   col = nypC,   proj = false, lvl = true, kz = true,  pl = array.new<line>(), pll = array.new<label>())
var array<Sess>  allS     = array.from(sAsia, sLon, sNya, sLc, sNyp)
var array<Liq>   liqs     = array.new<Liq>()
var array<Fvg>   fvgs     = array.new<Fvg>()
var array<Zone>  zones    = array.new<Zone>()
var array<Setup> setups   = array.new<Setup>()
var array<box>   miscBx   = array.new<box>()     // VI / BPR / Unicorn boxes
var array<line>  stLines  = array.new<line>()
var array<label> stLbls   = array.new<label>()
var array<label> evLbls   = array.new<label>()   // raid / judas / smt markers
var array<label> mkLbls   = array.new<label>()   // Break? / Run / Raid markers
var array<label> dispLbls = array.new<label>()   // displacement markers
var array<line>  pdLines  = array.new<line>()
var array<box>   pdBoxes  = array.new<box>()
var array<label> pdLbls   = array.new<label>()
var array<float> pdP      = array.new<float>()
var array<int>   wins     = array.new_int(5, 0)
var array<int>   losses   = array.new_int(5, 0)
var array<int>   capRej   = array.new_int(1, 0)      // attempts rejected because 40 trades were already open / pending (one setup may be retried, so this counts attempts)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// BASIC SERIES
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
float atr   = ta.atr(14)
float rng150 = ta.highest(high, 150) - ta.lowest(low, 150)
int   ageMs = ageMin * 60000
float pivH  = ta.pivothigh(high, swLen, swLen)
float pivL  = ta.pivotlow(low, swLen, swLen)
bool  intra = timeframe.isintraday and timeframe.in_seconds() <= 14400
float body  = math.abs(close - open)
bool midBull = close > open and body >= fvgMid * atr      // candidate middle candle of a bullish FVG (read at [1])
bool midBear = close < open and body >= fvgMid * atr
float crng  = high - low
bool dispBull = close > open and crng > 0 and body >= dispATR * atr and body / crng >= dispBody
bool dispBear = close < open and crng > 0 and body >= dispATR * atr and body / crng >= dispBody
// previous confirmed higher-timeframe bar only (lookahead_on + [1]) -> the bias cannot repaint
int htfB = request.security(syminfo.tickerid, biasTF, close[1] > ta.ema(close, biasLen)[1] ? 1 : -1, lookahead = barmerge.lookahead_on, ignore_invalid_symbol = true)
f_in(string r) => not na(time(timeframe.period, r, tzIn))

bool inAsia = intra and f_in(asiaR)
bool inLon  = intra and f_in(lonR)
bool inNya  = intra and f_in(nyaR)
bool inLc   = intra and f_in(lcR)
bool inNyp  = intra and f_in(nypR)
bool inSb1  = intra and f_in(sb1R)
bool inSb2  = intra and f_in(sb2R)
bool inSb3  = intra and f_in(sb3R)
bool sbAny   = sbOn and (inSb1 or inSb2 or inSb3)
bool kzAny   = (lonOn and inLon) or (nyaOn and inNya) or (lcOn and inLc) or (nypOn and inNyp) or sbAny

bool newDay = bar_index > 0 and dayofmonth(time, tzAnc) != dayofmonth(time[1], tzAnc) and not (syminfo.type != 'crypto' and dayofweek(time, tzAnc) == dayofweek.sunday)
int  wdNow  = syminfo.type == 'crypto' ? (dayofweek(time, tzAnc) + 5) % 7 : dayofweek(time, tzAnc) - 1
int  wdPrev = syminfo.type == 'crypto' ? (dayofweek(time[1], tzAnc) + 5) % 7 : dayofweek(time[1], tzAnc) - 1
// trading week: starts Sunday evening for forex / gold / indices (Sun = 0 ... Sat = 6), Monday 00:00 for crypto (Mon = 0 ... Sun = 6)
bool newWk  = bar_index > 0 and wdNow < wdPrev
bool newMth = bar_index > 0 and month(time, tzAnc) != month(time[1], tzAnc)
int  minDay = hour(time, tzAnc) * 60 + minute(time, tzAnc)
int  minDay1 = hour(time[1], tzAnc) * 60 + minute(time[1], tzAnc)
bool dayChg = bar_index > 0 and dayofmonth(time, tzAnc) != dayofmonth(time[1], tzAnc)
bool sunSkip = syminfo.type != 'crypto' and dayofweek(time, tzAnc) == dayofweek.sunday
bool wkndGap = bar_index > 0 and (time - time[1]) >= 24 * 3600 * 1000 and timeframe.isintraday
bool ndogBar = bar_index > 0 and timeframe.isintraday and minDay >= 1020 and minDay1 < 1020 and not dayChg

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// HELPERS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
f_px(float p) => str.tostring(p, format.mintick)
f_trimBox(array<box> a, int cap) =>
    while array.size(a) > cap
        box.delete(array.shift(a))
f_trimLbl(array<label> a, int cap) =>
    while array.size(a) > cap
        label.delete(array.shift(a))
f_trimLine(array<line> a, int cap) =>
    while array.size(a) > cap
        line.delete(array.shift(a))
f_mark(int x, float y, string txt, color c, string st) =>
    label m = label.new(x, y, txt, style = st, color = color.new(c, 100), textcolor = c, size = lsz)
    array.push(evLbls, m)
    f_trimLbl(evLbls, 60)
f_markD(int x, float y, string txt, color c, string st) =>
    label m = label.new(x, y, txt, style = st, color = color.new(c, 100), textcolor = c, size = lsz)
    array.push(dispLbls, m)
    f_trimLbl(dispLbls, 12)
f_markL(int x, float y, string txt, color c, string st) =>
    // several markers on the same bar are stacked (never printed on top of each other)
    int cnt = 0
    int nm = array.size(mkLbls)
    if nm > 0
        for k = 0 to nm - 1
            if label.get_x(array.get(mkLbls, k)) == x
                cnt += 1
    float off = cnt * 0.7 * nz(atr, 0)
    label m = label.new(x, st == label.style_label_down ? y + off : y - off, txt, style = st, color = color.new(c, 100), textcolor = c, size = lsz)
    array.push(mkLbls, m)
    f_trimLbl(mkLbls, maxLiqE + 10)
    m

f_cl(int x) => math.max(x, bar_index - 1400)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// LIQUIDITY LEVELS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
f_addLiq(float px, string nm, int d, int kind, int ob, color c) =>
    if not na(px)
        int i = array.size(liqs)
        while i > 0
            i -= 1
            Liq q = array.get(liqs, i)
            if q.name == nm
                array.remove(liqs, i)
                line.delete(q.ln)
                label.delete(q.lb)
                if q.pend and not na(q.mk)
                    label.delete(q.mk)
        // make room: the oldest level that is NOT waiting for its verdict goes first
        while array.size(liqs) >= maxLiqE
            int idx = -1
            int j = 0
            while j < array.size(liqs) and idx < 0
                Liq cq = array.get(liqs, j)
                if not cq.pend
                    idx := j
                j += 1
            if idx < 0
                break
            Liq q0 = array.get(liqs, idx)
            array.remove(liqs, idx)
            line.delete(q0.ln)
            label.delete(q0.lb)
        line   l = line.new(f_cl(ob), px, bar_index + lblPad - 1, px, color = c, style = line.style_dashed, width = 1)
        label  t = label.new(bar_index + 2, px, nm + ' ' + f_px(px), style = label.style_label_left, color = color.new(c, 100), textcolor = c, size = lsz)
        array.push(liqs, Liq.new(price = px, name = nm, dir = d, kind = kind, bar = bar_index, ln = l, lb = t))

f_kindScore(int k) => k == 3 or k == 4 ? 4 : k == 2 ? 3 : k == 1 ? 2 : 1

f_confirmRaid(Liq q, float ext) =>
    int sc = f_kindScore(q.kind)
    float pen = math.abs(ext - q.price)
    // one raid per bar: the highest-ranked level wins; equal rank -> the deeper penetration wins
    bool sameBar = S.raidScBar == bar_index
    bool better = not sameBar or sc > S.raidSc or (sc == S.raidSc and pen > S.raidPen)
    if not na(q.mk)
        label.delete(q.mk)
        q.mk := na
    if better
        // the marker of the level that just lost is removed, so only the winning raid stays on the chart
        if sameBar and not na(S.raidMk)
            label.delete(S.raidMk)
            S.raidMk := na
        S.raidDir := q.dir == 1 ? -1 : 1
        S.raidBar := bar_index
        S.raidT := time
        S.raidPx := ext
        S.raidName := q.name
        S.raidRej := true
        S.raidSc := sc
        S.raidPen := pen
        S.raidScBar := bar_index
        S.evRaid := S.raidDir
        if raidLbl
            S.raidMk := f_markL(bar_index, q.dir == 1 ? high : low, 'Raid ' + q.name, C_ORANGE, q.dir == 1 ? label.style_label_down : label.style_label_up)

f_chkLiq() =>
    int i = array.size(liqs)
    while i > 0
        i -= 1
        Liq q = array.get(liqs, i)
        if not q.swept
            if bar_index > q.bar
                bool hit = q.dir == 1 ? high > q.price : low < q.price
                if hit
                    q.swept := true
                    q.sweptBar := bar_index
                    line.set_x2(q.ln, bar_index)
                    line.set_style(q.ln, line.style_dotted)
                    label.set_text(q.lb, '✓')
                    label.set_x(q.lb, bar_index)
                    bool rej = q.dir == 1 ? close < q.price : close > q.price
                    float ext = q.dir == 1 ? high : low
                    if rej
                        f_confirmRaid(q, ext)
                    else
                        // closed beyond the level: PENDING for pendBars bars. Back through the level = RAID, still beyond after that = confirmed RUN
                        q.pend := true
                        q.hitBar := bar_index
                        q.hitExt := ext
                        if raidLbl
                            q.mk := f_markL(bar_index, ext, 'Break? ' + q.name, C_GRAY, q.dir == 1 ? label.style_label_down : label.style_label_up)
        else
            if q.pend
                q.hitExt := q.dir == 1 ? math.max(q.hitExt, high) : math.min(q.hitExt, low)
                bool back = q.dir == 1 ? close < q.price : close > q.price
                if back
                    q.pend := false
                    f_confirmRaid(q, q.hitExt)
                else if bar_index - q.hitBar >= pendBars
                    q.pend := false
                    S.evRun := q.dir
                    if not na(q.mk)
                        label.set_text(q.mk, 'Run ' + q.name)
            if not q.pend and bar_index - q.sweptBar > keepSweptE
                array.remove(liqs, i)
                line.delete(q.ln)
                label.delete(q.lb)
    // stop-loss extreme of a confirmed raid keeps updating only for a limited window
    if S.raidRej and bar_index - S.raidBar <= mssWin
        if S.raidDir == 1
            S.raidPx := math.min(nz(S.raidPx, low), low)
        if S.raidDir == -1
            S.raidPx := math.max(nz(S.raidPx, high), high)

f_target(int d, float entry, float minDist) =>
    float best = na
    for q in liqs
        if not q.swept and q.dir == d
            if d == 1 and q.price >= entry + minDist
                if na(best) or q.price < best
                    best := q.price
            if d == -1 and q.price <= entry - minDist
                if na(best) or q.price > best
                    best := q.price
    best

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// SESSION HIGHS / LOWS (tracking only, no drawing)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
method upd(Sess s, bool inS) =>
    bool ended = false
    if s.on
        if inS
            if not s.active
                s.active := true
                s.hi := high
                s.lo := low
                s.hiBar := bar_index
                s.loBar := bar_index
            else
                if high > s.hi
                    s.hi := high
                    s.hiBar := bar_index
                if low < s.lo
                    s.lo := low
                    s.loBar := bar_index
        else
            if s.active
                s.active := false
                ended := true
                s.fHi := s.hi
                s.fLo := s.lo
                s.fHiBar := s.hiBar
                s.fLoBar := s.loBar
    ended

f_sessEnd(Sess s) =>
    if s.lvl and sessLvlOnE
        f_addLiq(s.fHi, s.name + ' H', 1, 1, s.fHiBar, sessLC)
        f_addLiq(s.fLo, s.name + ' L', -1, 1, s.fLoBar, sessLC)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// DAY / WEEK / MONTH LEVELS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
f_periods() =>
    if newDay
        if not na(S.curDH)
            if pdOn
                f_addLiq(S.curDH, 'PDH', 1, 2, S.curDHB, pdC)
                f_addLiq(S.curDL, 'PDL', -1, 2, S.curDLB, pdC)
        S.curDH := high
        S.curDL := low
        S.curDHB := bar_index
        S.curDLB := bar_index
        S.jdDone := false
        S.po3Hi := high
        S.po3Lo := low
        S.po3Bias := 0
    else if not sunSkip
        if na(S.curDH) or high > S.curDH
            S.curDH := high
            S.curDHB := bar_index
        if na(S.curDL) or low < S.curDL
            S.curDL := low
            S.curDLB := bar_index
    if newWk
        if not na(S.curWH)
            if pwOn
                f_addLiq(S.curWH, 'PWH', 1, 3, S.curWHB, pwC)
                f_addLiq(S.curWL, 'PWL', -1, 3, S.curWLB, pwC)
        S.curWH := high
        S.curWL := low
        S.curWHB := bar_index
        S.curWLB := bar_index
    else
        if na(S.curWH) or high > S.curWH
            S.curWH := high
            S.curWHB := bar_index
        if na(S.curWL) or low < S.curWL
            S.curWL := low
            S.curWLB := bar_index
    if newMth
        if not na(S.curMH)
            if pmOnE
                f_addLiq(S.curMH, 'PMH', 1, 4, S.curMHB, pmC)
                f_addLiq(S.curML, 'PML', -1, 4, S.curMLB, pmC)
        S.curMH := high
        S.curML := low
        S.curMHB := bar_index
        S.curMLB := bar_index
    else
        if na(S.curMH) or high > S.curMH
            S.curMH := high
            S.curMHB := bar_index
        if na(S.curML) or low < S.curML
            S.curML := low
            S.curMLB := bar_index

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ZONES (blocks)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
f_zoneCol(int kind, int d) =>
    color c = d == 1 ? obBullC : obBearC
    if kind == 2
        c := d == 1 ? C_CYAN : C_ORANGE
    if kind == 3
        c := d == 1 ? C_PURPLE : C_PINK
    if kind == 4
        c := C_YELLOW
    if kind == 5
        c := C_LIME
    if kind == 6
        c := C_GRAY
    c
f_zoneName(int kind, int d) =>
    string n = ''
    if kind == 1
        n := d == 1 ? 'Bull OB' : 'Bear OB'
    if kind == 2
        n := d == 1 ? 'Bull Breaker' : 'Bear Breaker'
    if kind == 3
        n := d == 1 ? 'Bull Mitigation' : 'Bear Mitigation'
    if kind == 4
        n := d == 1 ? 'Bull Rejection' : 'Bear Rejection'
    if kind == 5
        n := d == 1 ? 'Bull Propulsion' : 'Bear Propulsion'
    n
f_addZone(float top, float bot, int d, int kind, int bar, string nm) =>
    bool ok = (kind == 1 and obOn) or (kind == 2 and brkOn) or (kind == 3 and mitOnE) or (kind == 4 and rbOnE) or (kind == 5 and pbOnE) or (kind == 6 and vacOnE)
    Zone res = na
    bool dup = false           // the same candle can produce the same block twice (repeated breaks of one swing): keep one
    if ok and top > bot
        for q in zones
            if q.kind == kind and q.dir == d and q.bar == bar
                dup := true
                res := q
    if ok and top > bot and not dup
        color c = f_zoneCol(kind, d)
        while array.size(zones) >= zoneMaxE
            Zone z0 = array.shift(zones)
            box.delete(z0.bx)
            if not na(z0.mt)
                line.delete(z0.mt)
            if not na(z0.lb)
                label.delete(z0.lb)
        box bx = box.new(left = bar, top = top, right = bar_index, bottom = bot, extend = extend.right, border_color = color.new(c, 30), border_width = 1, bgcolor = color.new(c, 85))
        line mt = na
        if zoneMTE
            mt := line.new(bar, (top + bot) / 2, bar_index, (top + bot) / 2, extend = extend.right, color = color.new(c, 40), style = line.style_dotted, width = 1)
        label lb = label.new(bar, d == 1 ? top : bot, nm, style = d == 1 ? label.style_label_down : label.style_label_up, color = color.new(c, 100), textcolor = c, size = lsz)
        res := Zone.new(top = top, bot = bot, dir = d, kind = kind, bar = bar, born = bar_index, nm = nm, bx = bx, mt = mt, lb = lb)
        array.push(zones, res)
    res
f_findOff(int cBar, bool wantBear, int span) =>
    int base = bar_index - cBar
    int res = base
    bool found = false
    for k = 0 to span
        int o = base + k
        if o <= 1000 and not found
            bool isb = close[o] < open[o]
            if isb == wantBear
                res := o
                found := true
    res
f_blockFromOff(int o, int d) =>
    float t = high[o]
    float b = low[o]
    if zoneMode == 'Body only'
        t := math.max(open[o], close[o])
        b := math.min(open[o], close[o])
    if zoneMode == 'Open to extreme'
        if d == 1
            t := math.max(open[o], close[o])
        else
            b := math.min(open[o], close[o])
    [t, b]
f_zoneAt(int cBar, bool wantBear, int d, int kind, string nm) =>
    if bar_index - cBar <= 900
        int o = f_findOff(cBar, wantBear, 3)
        [t, b] = f_blockFromOff(o, d)
        f_addZone(t, b, d, kind, bar_index - o, nm)
f_updZones() =>
    array<float> pq = array.new<float>()   // deferred propulsion blocks: top, bot, dir
    int i = array.size(zones)
    while i > 0
        i -= 1
        Zone z = array.get(zones, i)
        bool dead = false
        if z.kind == 6
            // gap: filled once price trades through the whole zone
            if z.dir == 1
                dead := low <= z.bot
            else
                dead := high >= z.top
        else
            dead := z.dir == 1 ? close < z.bot : close > z.top
        if dead
            array.remove(zones, i)
            box.delete(z.bx)
            if not na(z.mt)
                line.delete(z.mt)
            if not na(z.lb)
                label.delete(z.lb)
        else
            if not z.touched
                bool tch = z.dir == 1 ? low <= z.top : high >= z.bot
                if tch and bar_index > z.bar
                    z.touched := true
                    box.set_bgcolor(z.bx, color.new(f_zoneCol(z.kind, z.dir), 92))
            // propulsion block: candle probes the block and closes away from it
            if pbOnE and not z.prop and z.kind >= 1 and z.kind <= 3 and bar_index > z.bar
                if z.dir == 1 and close > open and low <= z.top and low >= z.bot and close > z.top
                    z.prop := true
                    array.push(pq, math.min(open, z.top))
                    array.push(pq, low)
                    array.push(pq, 1)
                if z.dir == -1 and close < open and high >= z.bot and high <= z.top and close < z.bot
                    z.prop := true
                    array.push(pq, high)
                    array.push(pq, math.max(open, z.bot))
                    array.push(pq, -1)
    int n = array.size(pq)
    int k = 0
    while k + 2 < n
        int d = int(array.get(pq, k + 2))
        f_addZone(array.get(pq, k), array.get(pq, k + 1), d, 5, bar_index, d == 1 ? 'Bull Propulsion' : 'Bear Propulsion')
        k += 3

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// FAIR VALUE GAPS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
f_fvgCol(Fvg f) =>
    color c = f.dir == 1 ? fvgBullC : fvgBearC
    if f.state == 1
        c := f.dir == 1 ? C_CYAN : C_ORANGE
    c
f_delFvg(Fvg f) =>
    box.delete(f.bx)
    if not na(f.ce)
        line.delete(f.ce)
    if not na(f.lb)
        label.delete(f.lb)
f_newFvg(float top, float bot, int d, float ext) =>
    while array.size(fvgs) >= fvgKeepE
        Fvg f0 = array.shift(fvgs)
        f_delFvg(f0)
    color c = d == 1 ? fvgBullC : fvgBearC
    int lb0 = bar_index - 2
    box bx = box.new(left = lb0, top = top, right = bar_index, bottom = bot, extend = extend.right, border_color = color.new(c, 40), border_width = 1, bgcolor = color.new(c, 86))
    line ce = na
    if fvgCE
        ce := line.new(lb0, (top + bot) / 2, bar_index, (top + bot) / 2, extend = extend.right, color = color.new(c, 30), style = line.style_dotted, width = 1)
    label lb = label.new(lb0, d == 1 ? top : bot, 'FVG', style = d == 1 ? label.style_label_down : label.style_label_up, color = color.new(c, 100), textcolor = c, size = lsz)
    Fvg f = Fvg.new(top = top, bot = bot, dir = d, bar = bar_index, ext = ext, bx = bx, ce = ce, lb = lb)
    array.push(fvgs, f)
    f
f_fvgDetect() =>
    if fvgOn
        float minSz = fvgMinE * atr
        if low > high[2] and (low - high[2]) >= minSz and (not fvgQualE or (midBull[1] and close[1] > high[2]))
            Fvg f = f_newFvg(low, high[2], 1, math.min(low, math.min(low[1], low[2])))
            S.evFvg := 1
            S.nfTop := f.top
            S.nfBot := f.bot
            S.nfExt := f.ext
            S.nfDir := 1
            // BPR with an opposite active FVG
            if bprOnE
                for g in fvgs
                    if g.dir == -1 and g.state == 0 and not g.bp and bar_index - g.bar <= 40
                        float oT = math.min(f.top, g.top)
                        float oB = math.max(f.bot, g.bot)
                        if oT - oB >= minSz
                            f.bp := true
                            g.bp := true
                            box bb = box.new(left = math.min(g.bar, bar_index) - 2, top = oT, right = bar_index + 6, bottom = oB, border_color = C_PURPLE, border_width = 2, bgcolor = color.new(C_PURPLE, 78), text = 'BPR', text_color = C_PURPLE, text_size = lsz)
                            array.push(miscBx, bb)
                            f_trimBox(miscBx, 30)
        if high < low[2] and (low[2] - high) >= minSz and (not fvgQualE or (midBear[1] and close[1] < low[2]))
            Fvg f = f_newFvg(low[2], high, -1, math.max(high, math.max(high[1], high[2])))
            S.evFvg := -1
            S.nfTop := f.top
            S.nfBot := f.bot
            S.nfExt := f.ext
            S.nfDir := -1
            if bprOnE
                for g in fvgs
                    if g.dir == 1 and g.state == 0 and not g.bp and bar_index - g.bar <= 40
                        float oT = math.min(f.top, g.top)
                        float oB = math.max(f.bot, g.bot)
                        if oT - oB >= minSz
                            f.bp := true
                            g.bp := true
                            box bb = box.new(left = math.min(g.bar, bar_index) - 2, top = oT, right = bar_index + 6, bottom = oB, border_color = C_PURPLE, border_width = 2, bgcolor = color.new(C_PURPLE, 78), text = 'BPR', text_color = C_PURPLE, text_size = lsz)
                            array.push(miscBx, bb)
                            f_trimBox(miscBx, 30)
        // volume imbalance (bodies gap, wicks overlap)
        if viOnE
            float vs = 0.05 * atr
            if math.min(open, close) - math.max(open[1], close[1]) >= vs and low <= high[1]
                box vb = box.new(left = bar_index - 1, top = math.min(open, close), right = bar_index + 8, bottom = math.max(open[1], close[1]), border_color = color.new(C_BULL, 50), bgcolor = color.new(C_BULL, 90), text = 'VI', text_color = C_BULL, text_size = lsz)
                array.push(miscBx, vb)
                f_trimBox(miscBx, 30)
            if math.min(open[1], close[1]) - math.max(open, close) >= vs and high >= low[1]
                box vb = box.new(left = bar_index - 1, top = math.min(open[1], close[1]), right = bar_index + 8, bottom = math.max(open, close), border_color = color.new(C_BEAR, 50), bgcolor = color.new(C_BEAR, 90), text = 'VI', text_color = C_BEAR, text_size = lsz)
                array.push(miscBx, vb)
                f_trimBox(miscBx, 30)
f_fvgUpdate() =>
    int i = array.size(fvgs)
    while i > 0
        i -= 1
        Fvg f = array.get(fvgs, i)
        bool kill = false
        if bar_index > f.bar
            if f.state == 0
                // bullish gap (dir 1) is filled from above; bearish from below
                if f.dir == 1
                    if low < f.bot
                        if close < f.bot and ifvgOnE
                            f.state := 1
                            f.dir := -1
                            box.set_border_color(f.bx, color.new(C_ORANGE, 30))
                            box.set_bgcolor(f.bx, color.new(C_ORANGE, 86))
                            f.nm := 'IFVG'
                            label.set_text(f.lb, 'IFVG')
                            label.set_textcolor(f.lb, C_ORANGE)
                            label.set_style(f.lb, label.style_label_up)
                            label.set_y(f.lb, f.bot)
                        else
                            kill := true
                    else if fvgShrink and low < f.top
                        f.top := math.max(low, f.bot)
                        box.set_top(f.bx, f.top)
                        if not na(f.ce)
                            line.set_y1(f.ce, (f.top + f.bot) / 2)
                            line.set_y2(f.ce, (f.top + f.bot) / 2)
                else
                    if high > f.top
                        if close > f.top and ifvgOnE
                            f.state := 1
                            f.dir := 1
                            box.set_border_color(f.bx, color.new(C_CYAN, 30))
                            box.set_bgcolor(f.bx, color.new(C_CYAN, 86))
                            f.nm := 'IFVG'
                            label.set_text(f.lb, 'IFVG')
                            label.set_textcolor(f.lb, C_CYAN)
                            label.set_style(f.lb, label.style_label_down)
                            label.set_y(f.lb, f.top)
                        else
                            kill := true
                    else if fvgShrink and high > f.bot
                        f.bot := math.min(high, f.top)
                        box.set_bottom(f.bx, f.bot)
                        if not na(f.ce)
                            line.set_y1(f.ce, (f.top + f.bot) / 2)
                            line.set_y2(f.ce, (f.top + f.bot) / 2)
            else
                // inverted FVG dies when price closes back through the opposite side
                if f.dir == -1 and close > f.top
                    kill := true
                if f.dir == 1 and close < f.bot
                    kill := true
        if kill
            f_delFvg(f)
            array.remove(fvgs, i)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// UNICORN
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
f_unicorn() =>
    if uniOn
        for z in zones
            // only a breaker made by the CURRENT MSS, and a live (not inverted) FVG formed in the same displacement leg
            if z.kind == 2 and not z.uni and S.mssDir == z.dir and z.born >= S.mssBar and time - S.mssT <= ageMs
                for f in fvgs
                    if not z.uni and not f.uni and f.state == 0 and f.dir == z.dir and f.bar >= S.mssBar - 3 and f.bar <= S.mssBar + uniWin
                        float oT = math.min(z.top, f.top)
                        float oB = math.max(z.bot, f.bot)
                        if oT > oB
                            z.uni := true
                            f.uni := true
                            color c = f.dir == 1 ? C_CYAN : C_ORANGE
                            box ub = box.new(left = math.min(z.bar, f.bar), top = oT, right = bar_index + 10, bottom = oB, border_color = color.white, border_width = 2, bgcolor = color.new(c, 70), text = 'Unicorn', text_color = color.white, text_size = lsz)
                            array.push(miscBx, ub)
                            f_trimBox(miscBx, 30)
                            S.evUni := f.dir
                            S.uniTop := oT
                            S.uniBot := oB
                            S.uniDir := f.dir
                            S.uniBar := bar_index
                            S.uniDone := false
                            S.uniFar := f.dir == 1 ? z.bot : z.top

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// STRUCTURE: swings, EQH/EQL, rejection blocks, dealing range, MSS / BOS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
f_drawDealing() =>
    while array.size(pdBoxes) > 0
        box.delete(array.shift(pdBoxes))
    while array.size(pdLines) > 0
        line.delete(array.shift(pdLines))
    while array.size(pdLbls) > 0
        label.delete(array.shift(pdLbls))
    array.clear(pdP)
    if not na(S.swHi) and not na(S.swLo) and S.swHi > S.swLo
        float r = S.swHi - S.swLo
        float eq = S.swLo + r / 2
        S.eq := eq
        S.rngTop := S.swHi
        S.rngBot := S.swLo
        int x0 = f_cl(math.min(S.swHiBar, S.swLoBar))
        if pdZoneOnE
            array.push(pdBoxes, box.new(left = x0, top = S.swHi, right = bar_index, bottom = eq, extend = extend.right, border_color = color.new(C_BEAR, 100), bgcolor = color.new(C_BEAR, 94)))
            array.push(pdBoxes, box.new(left = x0, top = eq, right = bar_index, bottom = S.swLo, extend = extend.right, border_color = color.new(C_BULL, 100), bgcolor = color.new(C_BULL, 94)))
            array.push(pdLines, line.new(x0, eq, bar_index, eq, extend = extend.right, color = color.new(C_GRAY, 20), style = line.style_dashed, width = 1))
            array.push(pdLbls, label.new(bar_index + 1, S.swHi, 'Premium', style = label.style_label_left, color = color.new(C_BEAR, 100), textcolor = C_BEAR, size = lsz))
            array.push(pdP, S.swHi)
            array.push(pdLbls, label.new(bar_index + 1, eq, 'EQ ' + f_px(eq), style = label.style_label_left, color = color.new(C_GRAY, 100), textcolor = C_GRAY, size = lsz))
            array.push(pdP, eq)
            array.push(pdLbls, label.new(bar_index + 1, S.swLo, 'Discount', style = label.style_label_left, color = color.new(C_BULL, 100), textcolor = C_BULL, size = lsz))
            array.push(pdP, S.swLo)
        // OTE of the most recent leg
        S.oteFired := false
        if S.swLoBar < S.swHiBar
            S.oteDir := 1
            S.oteTop := S.swHi - oteLo * r
            S.oteBot := S.swHi - oteHi * r
            S.oteMid := S.swHi - 0.705 * r
            S.oteSl := S.swLo
            S.oteTp := S.swHi + 0.27 * r
        else
            S.oteDir := -1
            S.oteBot := S.swLo + oteLo * r
            S.oteTop := S.swLo + oteHi * r
            S.oteMid := S.swLo + 0.705 * r
            S.oteSl := S.swHi
            S.oteTp := S.swLo - 0.27 * r
        if oteOn
            color oc = S.oteDir == 1 ? C_BULL : C_BEAR
            array.push(pdBoxes, box.new(left = f_cl(math.max(S.swHiBar, S.swLoBar)), top = S.oteTop, right = bar_index, bottom = S.oteBot, extend = extend.right, border_color = color.new(oc, 40), border_width = 1, bgcolor = color.new(oc, 82)))
            array.push(pdLines, line.new(f_cl(math.max(S.swHiBar, S.swLoBar)), S.oteMid, bar_index, S.oteMid, extend = extend.right, color = color.new(oc, 20), style = line.style_dotted, width = 1))
            array.push(pdLbls, label.new(bar_index + 1, S.oteMid, 'OTE 70.5%', style = label.style_label_left, color = color.new(oc, 100), textcolor = oc, size = lsz))
            array.push(pdP, S.oteMid)
f_pivots() =>
    float ph = pivH
    float pl = pivL
    bool changed = false
    if not na(ph)
        S.pSwHi := S.swHi
        S.swHi := ph
        S.swHiBar := bar_index - swLen
        S.swHiX := false
        changed := true
        // EQH
        if eqOn and not na(S.pSwHi) and math.abs(ph - S.pSwHi) <= eqTol * atr
            f_addLiq(math.max(ph, S.pSwHi), 'EQH', 1, 5, S.swHiBar, eqC)
        // rejection block (upper wick of the swing-high candle)
        if rbOnE
            float wk = high[swLen] - math.max(open[swLen], close[swLen])
            float cr = high[swLen] - low[swLen]
            if cr > 0 and wk >= rbPct * cr and wk >= rbATR * atr
                f_addZone(high[swLen], math.max(open[swLen], close[swLen]), -1, 4, bar_index - swLen, 'Bear Rejection')
    if not na(pl)
        S.pSwLo := S.swLo
        S.swLo := pl
        S.swLoBar := bar_index - swLen
        S.swLoX := false
        changed := true
        if eqOn and not na(S.pSwLo) and math.abs(pl - S.pSwLo) <= eqTol * atr
            f_addLiq(math.min(pl, S.pSwLo), 'EQL', -1, 5, S.swLoBar, eqC)
        if rbOnE
            float wk = math.min(open[swLen], close[swLen]) - low[swLen]
            float cr = high[swLen] - low[swLen]
            if cr > 0 and wk >= rbPct * cr and wk >= rbATR * atr
                f_addZone(math.min(open[swLen], close[swLen]), low[swLen], 1, 4, bar_index - swLen, 'Bull Rejection')
    if changed
        f_drawDealing()
f_structLine(float px, int fromBar, string txt, color c) =>
    array.push(stLines, line.new(f_cl(fromBar), px, bar_index, px, color = c, style = line.style_dashed, width = 1))
    array.push(stLbls, label.new(math.round((fromBar + bar_index) / 2), px, txt, style = label.style_none, textcolor = c, size = lsz))
    f_trimLine(stLines, structKeepE)
    f_trimLbl(stLbls, structKeepE)
f_breaks() =>
    // bullish break of the last swing high
    if not na(S.swHi) and not S.swHiX and close > S.swHi
        S.swHiX := true
        S.evBrk := 1
        bool dOk = bar_index - S.dispBullBar <= 3
        bool isMss = S.trend == -1 and (dOk or not mssNeedD)
        string tx = isMss ? (dOk ? 'MSS ⚡' : 'MSS') : (S.trend == -1 ? 'CHoCH' : 'BOS')
        if (isMss and showMss) or (not isMss and showBosE)
            f_structLine(S.swHi, S.swHiBar, tx, C_BULL)
        if isMss
            S.mssDir := 1
            S.mssBar := bar_index
            S.mssT := time
            S.evMss := 1
            bool swept = not na(S.pSwLo) and not na(S.swLo) and S.swLo < S.pSwLo
            if swept
                f_zoneAt(S.swHiBar, false, 1, 2, 'Bull Breaker')
            else
                f_zoneAt(S.swLoBar, true, 1, 3, 'Bull Mitigation')
        else
            if S.trend == 1 or S.trend == 0
                f_zoneAt(S.swLoBar, true, 1, 1, 'Bull OB')
        S.trend := 1
    // bearish break of the last swing low
    if not na(S.swLo) and not S.swLoX and close < S.swLo
        S.swLoX := true
        S.evBrk := -1
        bool dOk = bar_index - S.dispBearBar <= 3
        bool isMss = S.trend == 1 and (dOk or not mssNeedD)
        string tx = isMss ? (dOk ? 'MSS ⚡' : 'MSS') : (S.trend == 1 ? 'CHoCH' : 'BOS')
        if (isMss and showMss) or (not isMss and showBosE)
            f_structLine(S.swLo, S.swLoBar, tx, C_BEAR)
        if isMss
            S.mssDir := -1
            S.mssBar := bar_index
            S.mssT := time
            S.evMss := -1
            bool swept = not na(S.pSwHi) and not na(S.swHi) and S.swHi > S.pSwHi
            if swept
                f_zoneAt(S.swLoBar, true, -1, 2, 'Bear Breaker')
            else
                f_zoneAt(S.swHiBar, false, -1, 3, 'Bear Mitigation')
        else
            if S.trend == -1 or S.trend == 0
                f_zoneAt(S.swHiBar, false, -1, 1, 'Bear OB')
        S.trend := -1
f_gaps() =>
    if vacOnE and bar_index > 0
        float g = open - close[1]
        if math.abs(g) >= gapATR * atr
            string nm = wkndGap ? 'NWOG' : (ndogBar ? 'NDOG' : 'Vacuum')
            int d = g > 0 ? 1 : -1
            Zone z = f_addZone(math.max(open, close[1]), math.min(open, close[1]), d, 6, bar_index - 1, nm)
            if not na(z)
                z.nm := nm
                label.set_text(z.lb, nm)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// MODELS & SETUPS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
f_score(int d, bool kz) =>
    int s = 0
    if kz
        s += 1
    if S.raidDir == d and time - S.raidT <= ageMs
        s += 1
    if S.mssDir == d and time - S.mssT <= ageMs
        s += 1
    if (d == 1 and bar_index - S.dispBullBar <= 20) or (d == -1 and bar_index - S.dispBearBar <= 20)
        s += 1
    if htfB == d
        s += 1
    if not na(S.eq)
        if (d == 1 and close < S.eq) or (d == -1 and close > S.eq)
            s += 1
    s
f_mname(int m) => m == 1 ? 'ICT 2022' : m == 2 ? 'Silver Bullet' : m == 3 ? 'Unicorn' : 'OTE'
f_newSetup(int model, int d, float entry, float sl, bool inKz, bool filled) =>
    bool made = false
    float risk = math.abs(entry - sl)
    bool dirOk = d == 1 ? sl < entry : sl > entry
    int sc = f_score(d, inKz)
    bool htfOk = not htfReq or htfB == d
    if sigOn and risk > 0 and dirOk and htfOk and sc >= minScore
        float tp = f_target(d, entry, risk * minRR)
        if na(tp)
            tp := entry + d * risk * fbRR
        float rr = math.abs(tp - entry) / risk
        if rr >= minRR - 0.0001
            // only FINISHED setups may be dropped (their result is already counted); pending / open trades are never removed
            int oi = 0
            while array.size(setups) >= 24 and oi < array.size(setups)
                Setup o = array.get(setups, oi)
                if o.state == 2
                    array.remove(setups, oi)
                    if showSetups
                        line.delete(o.lEn)
                        line.delete(o.lSl)
                        line.delete(o.lTp)
                        label.delete(o.lb)
                else
                    oi += 1
            if array.size(setups) < 40      // 40 open / pending trades is the real limit
                color cc = d == 1 ? C_BULL : C_BEAR
                line le = na
                line ls = na
                line lt = na
                label lb = na
                if showSetups
                    le := line.new(bar_index, entry, bar_index + 1, entry, color = C_BLUE, style = line.style_solid, width = 2)
                    ls := line.new(bar_index, sl, bar_index + 1, sl, color = C_BEAR, style = line.style_solid, width = 1)
                    lt := line.new(bar_index, tp, bar_index + 1, tp, color = C_BULL, style = line.style_solid, width = 1)
                    lb := label.new(bar_index + 1, entry, f_mname(model) + (d == 1 ? ' ▲ BUY ' : ' ▼ SELL ') + '★' + str.tostring(sc) + '/6  RR ' + str.tostring(rr, '#.#'), style = label.style_label_left, color = color.new(cc, 20), textcolor = color.white, size = lsz)
                Setup t = Setup.new(model = model, dir = d, entry = entry, sl = sl, tp = tp, born = bar_index, state = filled ? 1 : 0, score = sc, rr = rr, cost = costATR * atr, lEn = le, lSl = ls, lTp = lt, lb = lb)
                if filled
                    t.fillBar := bar_index
                array.push(setups, t)
                S.evSig := d
                S.evModel := f_mname(model)
                made := true
            else
                array.set(capRej, 0, array.get(capRej, 0) + 1)
    made
f_finish(Setup t, bool win) =>
    t.state := 2
    if win
        array.set(wins, t.model, array.get(wins, t.model) + 1)
    else
        array.set(losses, t.model, array.get(losses, t.model) + 1)
    if showSetups
        line.set_x2(t.lEn, bar_index)
        line.set_x2(t.lSl, bar_index)
        line.set_x2(t.lTp, bar_index)
        line.set_color(t.lEn, color.new(C_GRAY, 20))
        line.set_color(win ? t.lSl : t.lTp, color.new(C_GRAY, 70))
        label.set_text(t.lb, f_mname(t.model) + (t.dir == 1 ? ' ▲ ' : ' ▼ ') + (win ? 'TP ✔' : 'SL ✖'))
        label.set_color(t.lb, color.new(win ? C_BULL : C_BEAR, 20))
        label.set_x(t.lb, bar_index)
f_updSetups() =>
    int i = array.size(setups)
    while i > 0
        i -= 1
        Setup t = array.get(setups, i)
        if t.state == 0
            bool cancel = bar_index - t.born > expBars
            if t.dir == 1 and (close < t.sl or high >= t.tp)
                cancel := true
            if t.dir == -1 and (close > t.sl or low <= t.tp)
                cancel := true
            if cancel
                array.remove(setups, i)
                if showSetups
                    line.delete(t.lEn)
                    line.delete(t.lSl)
                    line.delete(t.lTp)
                    label.delete(t.lb)
            else
                if bar_index > t.born and ((t.dir == 1 and low <= t.entry) or (t.dir == -1 and high >= t.entry))
                    t.state := 1
                    t.fillBar := bar_index
                    bool slNow = t.dir == 1 ? low <= t.sl + t.cost : high >= t.sl - t.cost
                    if slNow
                        f_finish(t, false)
                    else if showSetups
                        label.set_text(t.lb, f_mname(t.model) + (t.dir == 1 ? ' ▲ filled ' : ' ▼ filled ') + '★' + str.tostring(t.score))
                if t.state == 0 and showSetups
                    line.set_x2(t.lEn, bar_index)
                    line.set_x2(t.lSl, bar_index)
                    line.set_x2(t.lTp, bar_index)
                    label.set_x(t.lb, bar_index)
        else if t.state == 1 and bar_index > t.fillBar
            bool slHit = t.dir == 1 ? low <= t.sl + t.cost : high >= t.sl - t.cost       // pessimistic: the stop is checked first
            bool tpHit = t.dir == 1 ? high >= t.tp + t.cost : low <= t.tp - t.cost
            if slHit
                f_finish(t, false)
            else if tpHit
                f_finish(t, true)
            else if showSetups
                line.set_x2(t.lEn, bar_index)
                line.set_x2(t.lSl, bar_index)
                line.set_x2(t.lTp, bar_index)
                label.set_x(t.lb, bar_index)
f_entryPx(float top, float bot, int d) =>
    entryMode == 'FVG edge' ? (d == 1 ? top : bot) : (top + bot) / 2
f_models() =>
    float buf = slBuf * atr
    // ── ICT 2022: raid → structure break (MSS/BOS) → FVG ───────────────────────────────────────────
    if m22On
        if S.evMss != 0 and S.raidDir == S.evMss and bar_index - S.raidBar <= mssWin and time - S.raidT <= ageMs and (not kzOnly or kzAny)
            S.w22Dir := S.evMss
            S.w22Bar := bar_index
            S.w22Px := S.raidPx
        if S.w22Dir != 0
            if bar_index - S.w22Bar > 3
                S.w22Dir := 0
            else
                int n = array.size(fvgs)
                bool done = false
                int i = n
                while i > 0 and not done
                    i -= 1
                    Fvg f = array.get(fvgs, i)
                    if f.dir == S.w22Dir and f.state == 0 and f.bar >= S.w22Bar
                        float en = f_entryPx(f.top, f.bot, f.dir)
                        float sl = S.w22Dir == 1 ? S.w22Px - buf : S.w22Px + buf
                        // the chain ends only when a setup was really made (or after 3 bars); a rejected FVG lets the next one be tried
                        if f_newSetup(1, S.w22Dir, en, sl, kzAny, false)
                            S.w22Dir := 0
                            done := true
    // ── Silver Bullet ──────────────────────────────────────────────────────
    if not sbAny
        S.sbFired := false
    if mSbOn and sbAny and not S.sbFired and S.evFvg != 0
        int d = S.nfDir
        bool aligned = (S.mssDir == d and time - S.mssT <= ageMs) or (S.raidDir == d and time - S.raidT <= ageMs) or not sbBias
        if aligned
            float en = f_entryPx(S.nfTop, S.nfBot, d)
            float sl = d == 1 ? S.nfExt - buf : S.nfExt + buf
            if f_newSetup(2, d, en, sl, true, false)
                S.sbFired := true
    // ── Unicorn ────────────────────────────────────────────────────────────
    // by design ONE pending Unicorn at a time: a newer one replaces an older one that is still waiting; the saved zone is not re-validated during the 3 retry bars
    // a detected Unicorn zone is retried for 3 bars if the setup was rejected (score / RR / HTF / capacity); it makes at most ONE setup
    if mUniOn and S.uniDir != 0 and not S.uniDone and bar_index - S.uniBar <= 3 and (not kzOnly or kzAny)
        int d = S.uniDir
        float en = entryMode == 'FVG edge' ? (d == 1 ? S.uniTop : S.uniBot) : (S.uniTop + S.uniBot) / 2
        float sl = d == 1 ? S.uniFar - buf : S.uniFar + buf
        if f_newSetup(3, d, en, sl, kzAny, false)
            S.uniDone := true
    // ── OTE ───────────────────────────────────────────────────────────────
    if mOteOn and S.oteDir != 0 and not S.oteFired and (not kzOnly or kzAny)
        bool origin = oteSL == 'Swing origin (100%)'
        if S.oteDir == 1 and low <= S.oteTop and close > S.oteBot and close > S.oteMid and close > open
            float slv = origin ? S.oteSl : math.min(low, S.oteBot)
            if f_newSetup(4, 1, close, slv - buf, kzAny, false)
                S.oteFired := true
        if S.oteDir == -1 and high >= S.oteBot and close < S.oteTop and close < S.oteMid and close < open
            float slv = origin ? S.oteSl : math.max(high, S.oteTop)
            if f_newSetup(4, -1, close, slv + buf, kzAny, false)
                S.oteFired := true
    f_updSetups()

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// MAIN (confirmed bars only → no repaint of signals)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S.evRaid := 0
S.evRun := 0
S.evMss := 0
S.evBrk := 0
S.evFvg := 0
S.evUni := 0
S.evSig := 0
S.evModel := ''

if barstate.isconfirmed
    if dispBull
        S.dispBullBar := bar_index
        if dispMarkE
            f_markD(bar_index, (open + close) / 2, '⚡', C_BULL, label.style_label_center)
    if dispBear
        S.dispBearBar := bar_index
        if dispMarkE
            f_markD(bar_index, (open + close) / 2, '⚡', C_BEAR, label.style_label_center)
    if intra
        bool e1 = sAsia.upd(inAsia)
        bool e2 = sLon.upd(inLon)
        bool e3 = sNya.upd(inNya)
        bool e4 = sLc.upd(inLc)
        bool e5 = sNyp.upd(inNyp)
        if e1
            f_sessEnd(sAsia)
        if e2
            f_sessEnd(sLon)
        if e3
            f_sessEnd(sNya)
        if e4
            f_sessEnd(sLc)
        if e5
            f_sessEnd(sNyp)
    f_periods()
    f_fvgDetect()
    f_fvgUpdate()
    f_pivots()
    f_chkLiq()
    f_breaks()
    f_updZones()
    f_gaps()
    f_unicorn()
    f_models()

f_place(array<label> ls, array<float> ps) =>
    int n = array.size(ps)
    if n > 0
        float gap = nz(rng150, 0) * lblGap / 100
        array<int> ord = array.sort_indices(ps, order.descending)
        float prevY = na
        for k = 0 to n - 1
            int ix = array.get(ord, k)
            float y = array.get(ps, ix)
            if not na(prevY) and prevY - y < gap
                y := prevY - gap
            label.set_xy(array.get(ls, ix), bar_index + lblPad, y)
            prevY := y

// every right-side label is re-stacked on the last bar: sorted by price, never closer than the minimum gap;
// blocks / FVGs far from the current price keep their box but lose the label, so the right column stays readable
f_spread() =>
    array<label> ls = array.new<label>()
    array<float> ps = array.new<float>()
    int xe = bar_index + lblPad - 1
    float near = nz(rng150, 0) * lblNearE
    for q in liqs
        if q.swept
            label.set_x(q.lb, q.sweptBar)
        else
            line.set_x2(q.ln, xe)
            if math.abs(q.price - close) <= near * 1.5
                label.set_text(q.lb, q.name + ' ' + f_px(q.price))
                array.push(ls, q.lb)
                array.push(ps, q.price)
            else
                label.set_text(q.lb, '')
    for z in zones
        if not na(z.lb)
            float mid = (z.top + z.bot) / 2
            if math.abs(mid - close) <= near
                label.set_text(z.lb, z.nm)
                label.set_style(z.lb, label.style_label_left)
                array.push(ls, z.lb)
                array.push(ps, mid)
            else
                label.set_text(z.lb, '')
    for f in fvgs
        if not na(f.lb)
            float mid = (f.top + f.bot) / 2
            if math.abs(mid - close) <= near
                label.set_text(f.lb, f.nm)
                label.set_style(f.lb, label.style_label_left)
                array.push(ls, f.lb)
                array.push(ps, mid)
            else
                label.set_text(f.lb, '')
    int np = array.size(pdLbls)
    if np > 0
        for k = 0 to np - 1
            array.push(ls, array.get(pdLbls, k))
            array.push(ps, array.get(pdP, k))
    if showSetups
        for t in setups
            if t.state < 2 and not na(t.lb)
                array.push(ls, t.lb)
                array.push(ps, t.entry)
    f_place(ls, ps)

if barstate.islast
    f_spread()

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// PANEL (active killzone, HTF bias, last raid / MSS, dealing range, win-rate)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var table pnl = na

f_row(table t, int r, string k, string v, color vc) =>
    table.cell(t, 0, r, k, text_color = color.new(color.gray, 0), text_size = psz, text_halign = text.align_left, bgcolor = color.new(BRAND_DARK, 8))
    table.cell(t, 1, r, v, text_color = vc, text_size = psz, text_halign = text.align_left, bgcolor = color.new(BRAND_DARK, 8))
f_dirTxt(int d) => d == 1 ? 'Bullish ▲' : d == -1 ? 'Bearish ▼' : '—'
f_dirCol(int d) => d == 1 ? C_BULL : d == -1 ? C_BEAR : color.gray

if barstate.islast and showPanel
    if na(pnl)
        pnl := table.new(panelPos, 2, 12, border_width = 1, border_color = color.new(color.gray, 70), frame_width = 1, frame_color = color.new(BRAND_GREEN, 20))
    string actNm = 'Outside killzones'
    color  actCol = color.gray
    for s in allS
        if s.on and s.active
            actNm := s.name
            actCol := s.col
    if sbAny
        actNm += ' + Silver Bullet'
    table.cell(pnl, 0, 0, 'Crypex ICT Models', text_color = BRAND_GREEN, text_size = psz, text_halign = text.align_left, text_formatting = text.format_bold, bgcolor = color.new(BRAND_DARK, 8))
    table.cell(pnl, 1, 0, intra ? '' : '⚠ use ≤ 4H', text_color = C_ORANGE, text_size = psz, text_halign = text.align_left, bgcolor = color.new(BRAND_DARK, 8))
    f_row(pnl, 1, 'Now', actNm, actCol)
    f_row(pnl, 2, 'HTF bias ' + biasTF + '/' + str.tostring(biasLen), f_dirTxt(htfB), f_dirCol(htfB))
    string rdTxt = S.raidDir == 0 ? '—' : S.raidName + (S.raidRej ? ' (raid)' : ' (run)') + ' · ' + str.tostring(bar_index - S.raidBar) + ' bars ago'
    f_row(pnl, 3, 'Last liquidity', rdTxt, f_dirCol(S.raidDir))
    string msTxt = S.mssDir == 0 ? '—' : f_dirTxt(S.mssDir) + ' · ' + str.tostring(bar_index - S.mssBar) + ' bars ago'
    f_row(pnl, 4, 'Last MSS', msTxt, f_dirCol(S.mssDir))
    string pdTxt = '—'
    color pdCol = color.gray
    if not na(S.rngTop) and S.rngTop > S.rngBot
        float pos = (close - S.rngBot) / (S.rngTop - S.rngBot) * 100
        if pos > 100
            pdTxt := 'Above range (new highs)'
            pdCol := C_BEAR
        else if pos < 0
            pdTxt := 'Below range (new lows)'
            pdCol := C_BULL
        else
            pdTxt := (pos >= 50 ? 'Premium ' : 'Discount ') + str.tostring(pos, '#') + '%'
            pdCol := pos >= 50 ? C_BEAR : C_BULL
    f_row(pnl, 5, 'Dealing range', pdTxt, pdCol)
    if showStatsE
        for m = 1 to 4
            int w = array.get(wins, m)
            int l = array.get(losses, m)
            int tot = w + l
            string wr = tot > 0 ? str.tostring(w) + 'W / ' + str.tostring(l) + 'L  (' + str.tostring(w * 100.0 / tot, '#') + '%)' : 'no closed trades'
            f_row(pnl, 5 + m, f_mname(m), wr, tot > 0 and w >= l ? C_BULL : (tot > 0 ? C_BEAR : color.gray))
        f_row(pnl, 10, 'Min score / RR', str.tostring(minScore) + '/6  ·  ' + str.tostring(minRR, '#.#') + 'R', color.gray)
    if sigOn and array.get(capRej, 0) > 0
        f_row(pnl, 11, 'Rejected: 40 open', str.tostring(array.get(capRej, 0)) + ' attempts', C_ORANGE)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ALERTS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
alertcondition(S.evRaid == -1, 'Buy-side liquidity raided', 'Crypex ICT: buy-side liquidity swept (bearish expectation)')
alertcondition(S.evRaid == 1, 'Sell-side liquidity raided', 'Crypex ICT: sell-side liquidity swept (bullish expectation)')
alertcondition(S.evRun != 0, 'Liquidity run (continuation)', 'Crypex ICT: liquidity level broken and held (run, not a raid)')
alertcondition(S.evMss == 1, 'Bullish MSS', 'Crypex ICT: bullish market structure shift')
alertcondition(S.evMss == -1, 'Bearish MSS', 'Crypex ICT: bearish market structure shift')
alertcondition(S.evFvg == 1, 'Bullish FVG', 'Crypex ICT: new bullish FVG')
alertcondition(S.evFvg == -1, 'Bearish FVG', 'Crypex ICT: new bearish FVG')
alertcondition(S.evUni != 0, 'Unicorn zone', 'Crypex ICT: unicorn zone formed')
alertcondition(S.evSig == 1, 'BUY setup', 'Crypex ICT: BUY setup')
alertcondition(S.evSig == -1, 'SELL setup', 'Crypex ICT: SELL setup')

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// WATERMARK
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var table wm = na
if showWatermark and barstate.islast
    if na(wm)
        wm := table.new(wmPos, 3, 1, border_width = 0, frame_width = 0)
    string wmSize = wmSizeStr == 'small' ? size.small : wmSizeStr == 'normal' ? size.normal : wmSizeStr == 'large' ? size.large : size.huge
    float spacerW = wmPos == position.bottom_left ? wmGap : 0.0
    bool bgLight = (color.r(chart.bg_color) * 299 + color.g(chart.bg_color) * 587 + color.b(chart.bg_color) * 114) / 1000 > 128
    color orgCol = bgLight ? BRAND_DARK : color.white
    table.cell(wm, 0, 0, '', width = spacerW, text_size = wmSize)
    table.cell(wm, 1, 0, 'crypex', text_color = color.new(BRAND_GREEN, wmTransp), text_size = wmSize, text_halign = text.align_right, text_valign = text.align_bottom, text_formatting = text.format_bold)
    table.cell(wm, 2, 0, '.org', text_color = color.new(orgCol, wmTransp), text_size = wmSize, text_halign = text.align_left, text_valign = text.align_bottom, text_formatting = text.format_bold)
ict-models-crypex.mq5 · MQL5 · ۲٬۰۲۷ خط
//+------------------------------------------------------------------+
//| Crypex ICT Models (MT5)                                        |
//| Market structure (MSS / BOS), Fair Value Gaps (CE, IFVG, VI,     |
//| BPR), Order Block, Breaker, Mitigation, Unicorn, Premium /       |
//| Discount, OTE, Raid / Run, panel. Analysis only by default;      |
//| ICT 2022 / Silver Bullet / Unicorn / OTE setups are optional     |
//| © Crypex.org — All rights reserved. | حق نشر محفوظ برای Crypex.org |
//| License: personal, non-commercial use only. Redistribution or    |
//| resale without written permission is prohibited.                 |
//| مجوز: استفاده شخصی و غیرتجاری. بازنشر یا فروش مجدد بدون          |
//| اجازه‌ی کتبی ممنوع است.                                           |
//| Educational tool, not investment advice.                         |
//+------------------------------------------------------------------+
#property copyright "Crypex.org"
#property link      "https://crypex.org"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

enum ENUM_TM_DST    { TM_DST_US, TM_DST_EU, TM_DST_NONE };
enum ENUM_TM_ZONE   { TM_Z_TEHRAN, TM_Z_NY, TM_Z_UTC, TM_Z_LONDON };
enum ENUM_TM_MARKET { TM_M_AUTO, TM_M_FOREX, TM_M_METALS, TM_M_CRYPTO };
enum ENUM_TM_CORNER { TM_C_AUTO, TM_C_TOPRIGHT, TM_C_TOPLEFT, TM_C_BOTTOMRIGHT, TM_C_BOTTOMLEFT };
enum ENUM_MD_PRESET { MD_P_CLEAN, MD_P_STANDARD, MD_P_FULL, MD_P_CUSTOM };
enum ENUM_MD_ZMODE  { MD_Z_FULL, MD_Z_BODY, MD_Z_OPEN };
enum ENUM_MD_ENTRY  { MD_E_CE, MD_E_EDGE };
enum ENUM_MD_OTESL  { MD_O_BEYOND, MD_O_ORIGIN };

input group "General & time zones"
input ENUM_MD_PRESET   InpPreset       = MD_P_CLEAN;      // Preset (Clean = only the most valuable items, Standard, Full, Custom = my own settings below)
input int              InpBrokerGmt    = 2;               // Broker server time: GMT offset in WINTER (e.g. 2 for GMT+2 / GMT+3 brokers)
input ENUM_TM_DST      InpBrokerDst    = TM_DST_US;       // Broker daylight-saving rule (US = GMT+2/+3 New-York-close brokers)
input ENUM_TM_ZONE     InpTzDisp       = TM_Z_TEHRAN;     // Show times on chart in
input ENUM_TM_MARKET   InpMarket       = TM_M_AUTO;       // Market type (Auto = detect from the symbol name)
input int              InpFont         = 9;               // Label font size
input double           InpLblGap       = 3.0;             // Right-side labels: min vertical gap (% of recent range)
input int              InpLblPad       = 3;               // Right-side labels: offset from last bar (bars)
input double           InpLblNear      = 0.30;            // Show zone labels within (x recent range from price)
input int              InpRightMargin  = 15;              // Chart right margin (% of the chart, 0 = leave the chart alone)
input group "Killzones (used by the models, New York time)"
input bool             InpAsiaOn       = true;            // Asian range
input string           InpAsiaR        = "2000-0000";     //   Asian range
input bool             InpLonOn        = true;            // London killzone
input string           InpLonR         = "0200-0500";     //   London
input bool             InpNyaOn        = true;            // New York AM killzone
input string           InpNyaR         = "0700-1000";     //   NY AM
input bool             InpLcOn         = true;            // London Close killzone
input string           InpLcR          = "1000-1200";     //   London Close
input bool             InpNypOn        = true;            // New York PM killzone
input string           InpNypR         = "1330-1600";     //   NY PM
input bool             InpSessLvlOn    = true;            // Turn finished session highs/lows into liquidity levels (Custom preset)
input bool             InpSbOn         = true;            // Silver Bullet windows
input string           InpSb1R         = "0300-0400";     //   SB 1 (London)
input string           InpSb2R         = "1000-1100";     //   SB 2 (NY AM)
input string           InpSb3R         = "1400-1500";     //   SB 3 (NY PM)
input group "Liquidity levels (raids & targets)"
input bool             InpPdOn         = true;            // PDH / PDL
input color            InpPdC          = C'41,98,255';
input bool             InpPwOn         = true;            // PWH / PWL
input color            InpPwC          = C'156,39,176';
input bool             InpPmOn         = false;           // PMH / PML (Custom preset)
input color            InpPmC          = C'233,30,99';
input color            InpSessLC       = C'120,123,134';
input bool             InpEqOn         = true;            // EQH / EQL (equal highs / lows)
input color            InpEqC          = C'255,109,0';
input double           InpEqTol        = 0.10;            // EQ tolerance (x ATR)
input bool             InpRaidLbl      = true;            // Mark raids and runs
input int              InpPendBars     = 3;               // Bars to wait before a break counts as a Run
input int              InpKeepSwept    = 60;              // Keep swept levels for (bars) (Custom preset)
input int              InpMaxLiq       = 40;              // Max liquidity levels on chart (Custom preset)
input group "Market structure & displacement"
input int              InpSwLen        = 5;               // Swing length
input double           InpDispAtr      = 1.2;             // Displacement: body >= x ATR
input double           InpDispBody     = 0.6;             // Displacement: body / range >=
input bool             InpDispMark     = true;            // Mark displacement candles (Custom preset)
input bool             InpMssNeedD     = false;           // MSS needs displacement (else a plain break = BOS / CHoCH)
input bool             InpShowBos      = true;            // Show BOS (Custom preset)
input bool             InpShowMss      = true;            // Show MSS
input int              InpStructKeep   = 14;              // Structure lines to keep (Custom preset)
input group "Premium / Discount & OTE"
input bool             InpPdZoneOn     = true;            // Premium / Discount / Equilibrium zone (Custom preset)
input bool             InpOteOn        = true;            // OTE zone
input double           InpOteLo        = 0.62;            // OTE from
input double           InpOteHi        = 0.79;            // OTE to
input group "Fair Value Gaps"
input bool             InpFvgOn        = true;            // Fair Value Gaps
input double           InpFvgMin       = 0.10;            // Min FVG size (x ATR) (Custom preset)
input int              InpFvgKeep      = 16;              // Max active FVGs (Custom preset)
input bool             InpFvgQual      = false;           // Quality filter: middle candle must be a strong move (Custom preset)
input double           InpFvgMid       = 0.5;             // Quality filter: middle candle body >= x ATR
input bool             InpFvgCE        = true;            // Show Consequent Encroachment (50 %)
input bool             InpFvgShrink    = true;            // Shrink FVG when partly filled
input bool             InpIfvgOn       = true;            // Inversion FVG (IFVG) (Custom preset)
input bool             InpViOn         = true;            // Volume Imbalance (VI) (Custom preset)
input bool             InpBprOn        = true;            // Balanced Price Range (BPR) (Custom preset)
input color            InpFvgBullC     = C'8,153,129';
input color            InpFvgBearC     = C'242,54,69';
input group "Blocks, Breaker, Mitigation, Rejection, Propulsion, Gaps, Unicorn"
input bool             InpObOn         = true;            // Order Block (BOS continuation)
input bool             InpBrkOn        = true;            // Breaker Block (approx.)
input bool             InpMitOn        = true;            // Mitigation Block (approx.) (Custom preset)
input bool             InpRbOn         = true;            // Rejection Block (Custom preset)
input bool             InpPbOn         = true;            // Propulsion Block (Custom preset)
input bool             InpVacOn        = true;            // Vacuum Block / NWOG / NDOG (Custom preset)
input bool             InpUniOn        = true;            // Unicorn (Breaker + FVG overlap)
input ENUM_MD_ZMODE    InpZoneMode     = MD_Z_FULL;       // Block zone from
input bool             InpZoneMt       = true;            // Show 50 % mean threshold of blocks (Custom preset)
input int              InpZoneMax      = 24;              // Max blocks on chart (Custom preset)
input double           InpRbPct        = 0.5;             // Rejection: wick >= % of candle
input double           InpRbAtr        = 0.5;             // Rejection: wick >= x ATR
input double           InpGapAtr       = 0.3;             // Gap: min size (x ATR)
input color            InpObBullC      = C'8,153,129';
input color            InpObBearC      = C'242,54,69';
input group "Models & signals"
input bool             InpSigOn        = false;           // Trade setups: Entry / SL / TP signals (OFF = analysis only)
input bool             InpM22On        = true;            // Model: ICT 2022 (raid -> MSS -> FVG)
input bool             InpMSbOn        = true;            // Model: Silver Bullet (FVG inside window)
input bool             InpMUniOn       = true;            // Model: Unicorn
input bool             InpMOteOn       = true;            // Model: OTE
input int              InpUniWin       = 6;               // Unicorn: FVG must form within N bars after the MSS
input bool             InpKzOnly       = true;            // Signals only inside killzones / Silver Bullet
input int              InpMssWin       = 30;              // ICT 2022: max bars from raid to MSS
input ENUM_MD_ENTRY    InpEntryMode    = MD_E_CE;         // Entry on FVG
input double           InpSlBuf        = 0.10;            // Stop buffer (x ATR)
input double           InpMinRR        = 1.5;             // Min reward : risk
input double           InpFbRR         = 2.0;             // Fallback target (R) if no liquidity target
input int              InpExpBars      = 40;              // Cancel unfilled setup after (bars)
input int              InpMinScore     = 3;               // Min confluence score (0-6)
input bool             InpSbBias       = true;            // Silver Bullet needs raid or MSS in the same direction
input ENUM_TIMEFRAMES  InpBiasTF       = PERIOD_H4;       // HTF bias timeframe
input int              InpBiasLen      = 21;              // HTF bias EMA length
input ENUM_MD_OTESL    InpOteSL        = MD_O_BEYOND;     // OTE model stop
input int              InpAgeMin       = 240;             // Event validity for the score (minutes)
input bool             InpHtfReq       = false;           // Require HTF bias in the trade direction
input double           InpCostAtr      = 0.03;            // Trading cost (x ATR) in the win-rate stats
input bool             InpShowSetups   = true;            // Draw entry / SL / TP
input group "Panel & watermark"
input bool             InpShowPanel    = true;            // Show info panel
input ENUM_TM_CORNER   InpPanelPos     = TM_C_AUTO;       // Panel position (Auto = the emptiest corner of the visible chart)
input bool             InpShowStats    = false;           // Win / loss stats of the models in the panel (Custom preset, needs trade setups)
input bool             InpShowWm       = true;            // Show crypex.org watermark
input int              InpWmSize       = 18;
input int              InpWmGap        = 60;              // Gap from left (px), clears the chart logo
input int              InpWmBottom     = 14;              // Gap from bottom (px)
input group "Alerts"
input bool             InpAlertPopup   = true;
input bool             InpAlertPush    = false;
input bool             InpAlertMail    = false;
input group "Calculation"
input int              InpMaxBars      = 6000;            // Bars used for calculation

const string PFX = "CRX_MODELS_";
const double TM_NA = DBL_MAX;
const color  C_BULL = C'8,153,129', C_BEAR = C'242,54,69', C_BLUE = C'41,98,255', C_ORANGE = C'255,152,0', C_PURPLE = C'156,39,176';
const color  C_YELLOW = C'255,214,0', C_GRAY = C'120,123,134', C_CYAN = C'0,188,212', C_PINK = C'233,30,99';
const color  BRAND_GREEN = C'11,163,28', BRAND_DARK = C'30,34,41';

struct TmSess { string name; int a; int b; bool on; color col; bool proj; bool lvl; bool kz; bool active; int sBar; datetime sTime;
                double hi; double lo; int hiBar; int loBar; double fHi; double fLo; int fHiBar; int fLoBar; int boxId; };
struct TmBox  { int id; int s; int i0; int i1; double hi; double lo; datetime sTime; };
struct TmLiq  { double price; string name; int dir; int kind; int bar; int ob; bool swept; int sweptBar; bool pend; int hitBar; double hitExt; int mk; };
struct TmMark { int id; int bar; double price; string text; color col; int dir; int kind; bool alive; };
struct TmProj { bool ok; int i0; double hi; double lo; };
struct TmAnc  { string name; bool on; color col; bool has; int bar; double px; bool exact; };
struct TmAncH { int x1; int x2; double px; color col; };
struct TmItem { string text; color col; double price; };

// ---- chart copy (oldest first) and per-bar derived series
datetime g_t[];
double   g_o[], g_h[], g_l[], g_c[];
double   g_atr[], g_x2h[], g_x2l[];
datetime g_ny[];                       // New York wall clock of every bar (as a plain datetime)
int      g_mn[], g_dow[], g_day[], g_mon[], g_fl[];
int      g_n = 0;

TmSess   g_s[8];
TmBox    g_box[];   int g_nBox = 0;   int g_boxId = 0;
TmLiq    g_lq[];    int g_nLq = 0;
TmMark   g_mk[];    int g_nMk = 0;    int g_mkId = 0;
TmProj   g_pj[8];
TmAnc    g_an[4];
TmAncH   g_ah[];    int g_nAh = 0;

bool     g_intra = true, g_isCrypto = false, g_isForex = false;
// state (St object of the Pine version)
double   g_swHi = TM_NA, g_pSwHi = TM_NA, g_swLo = TM_NA, g_pSwLo = TM_NA;
int      g_swHiBar = 0, g_swLoBar = 0;
int      g_raidDir = 0, g_raidBar = -9999, g_raidSc = 0, g_raidScBar = -1, g_raidMk = -1;
double   g_raidPx = TM_NA, g_raidPen = 0.0;
string   g_raidName = "";
bool     g_raidRej = false;
double   g_curDH = TM_NA, g_curDL = TM_NA, g_curWH = TM_NA, g_curWL = TM_NA, g_curMH = TM_NA, g_curML = TM_NA;
int      g_curDHB = 0, g_curDLB = 0, g_curWHB = 0, g_curWLB = 0, g_curMHB = 0, g_curMLB = 0;
bool     g_jdDone = false;
int      g_jdPot = 0;
double   g_jdExt = TM_NA, g_moPx = TM_NA;
int      g_po3DnBar = -9999, g_po3UpBar = -9999, g_po3Bias = 0;
double   g_po3DnExt = TM_NA, g_po3UpExt = TM_NA, g_po3Ext = TM_NA;
double   g_phM = TM_NA, g_phS = TM_NA, g_plM = TM_NA, g_plS = TM_NA;
int      g_smtDir = 0, g_smtBar = -9999;
int      g_evRaid = 0, g_evRun = 0, g_evJudas = 0, g_evSmt = 0;
bool     g_al[10];
bool     g_alLast[10];
color    g_bg = clrBlack;
datetime g_lastBar = 0, g_lastAlertBar = 0;
bool     g_firstDone = false;
int      g_cnt = 0;
string   g_macroR[9] = {"0233-0300", "0403-0430", "0850-0910", "0950-1010", "1050-1110", "1150-1210", "1310-1340", "1450-1510", "1515-1545"};
int      g_macroA[9], g_macroB[9];
int      g_sb1A, g_sb1B, g_sb2A, g_sb2B, g_sb3A, g_sb3B, g_jdA, g_jdB, g_poAA, g_poAB, g_poMA, g_poMB, g_poDA, g_poDB;


const color C_LIME = C'139,195,74';
bool   EDispMark = false, EShowBos = false, EIfvg = false, EVi = false, EBpr = false, EMit = false, ERb = false, EPb = false, EVac = false;
bool   EZoneMt = false, EPdZone = false, ESessLvl = false, EPm = false, EQual = true, EStats = false;
int    EFvgKeep = 5, EZoneMax = 6, EMaxLiq = 12, EKeepSwept = 20, EStructKeep = 6;
double ELblNear = 0.25, EFvgMin = 0.3;

datetime g_raidT = 0;

//+------------------------------------------------------------------+
//| time zones (MT has no time-zone database, so the rules are coded) |
//+------------------------------------------------------------------+
datetime NthSunday(const int y, const int m, const int n)
  {
   MqlDateTime d; d.year = y; d.mon = m; d.day = 1; d.hour = 0; d.min = 0; d.sec = 0;
   datetime first = StructToTime(d);
   MqlDateTime f; TimeToStruct(first, f);
   int add = (7 - f.day_of_week) % 7;
   return first + (datetime)((add + 7 * (n - 1)) * 86400);
  }

datetime LastSunday(const int y, const int m)
  {
   MqlDateTime d; d.year = (m == 12 ? y + 1 : y); d.mon = (m == 12 ? 1 : m + 1); d.day = 1; d.hour = 0; d.min = 0; d.sec = 0;
   datetime lastDay = StructToTime(d) - 86400;
   MqlDateTime f; TimeToStruct(lastDay, f);
   return lastDay - (datetime)(f.day_of_week * 86400);
  }

bool UsDstUtc(const datetime u)               // 2nd Sunday of March 02:00 EST  ->  1st Sunday of November 02:00 EDT
  {
   MqlDateTime d; TimeToStruct(u, d);
   datetime s = NthSunday(d.year, 3, 2) + 7 * 3600;
   datetime e = NthSunday(d.year, 11, 1) + 6 * 3600;
   return u >= s && u < e;
  }

bool EuDstUtc(const datetime u)               // last Sunday of March 01:00 UTC  ->  last Sunday of October 01:00 UTC
  {
   MqlDateTime d; TimeToStruct(u, d);
   return u >= LastSunday(d.year, 3) + 3600 && u < LastSunday(d.year, 10) + 3600;
  }

bool BrokerDstAt(const datetime u)
  {
   if(InpBrokerDst == TM_DST_US) return UsDstUtc(u);
   if(InpBrokerDst == TM_DST_EU) return EuDstUtc(u);
   return false;
  }

datetime ServerToUtc(const datetime ts)
  {
   datetime summer = ts - (datetime)((InpBrokerGmt + 1) * 3600);
   if(BrokerDstAt(summer)) return summer;
   return ts - (datetime)(InpBrokerGmt * 3600);
  }

datetime ToNY(const datetime u)  { return u + (datetime)(UsDstUtc(u) ? -4 * 3600 : -5 * 3600); }

datetime NyToUtc(const datetime ny)
  {
   datetime u = ny + 5 * 3600;
   if(ToNY(u) != ny) u = ny + 4 * 3600;
   return u;
  }

datetime ToZone(const datetime u, const ENUM_TM_ZONE z)
  {
   if(z == TM_Z_NY) return ToNY(u);
   if(z == TM_Z_UTC) return u;
   if(z == TM_Z_LONDON) return u + (datetime)(EuDstUtc(u) ? 3600 : 0);
   return u + 12600;                           // Tehran: UTC+3:30, no daylight saving
  }

datetime NowUtc()
  {
   datetime g = TimeGMT();
   if(g > 0) return g;
   return ServerToUtc(TimeCurrent());
  }

string Dn() { return ShortToString(0x25BC); }
string Up() { return ShortToString(0x25B2); }
string Ck() { return ShortToString(0x2713); }

string Num2(const double v)
  {
   string t = DoubleToString(v, 2);
   while(StringLen(t) > 0 && StringSubstr(t, StringLen(t) - 1, 1) == "0") t = StringSubstr(t, 0, StringLen(t) - 1);
   if(StringLen(t) > 0 && StringSubstr(t, StringLen(t) - 1, 1) == ".") t = StringSubstr(t, 0, StringLen(t) - 1);
   return t;
  }

string Two(const int v) { return (v < 10 ? "0" : "") + IntegerToString(v); }

string Hm(const datetime clock)
  {
   MqlDateTime d; TimeToStruct(clock, d);
   return Two(d.hour) + ":" + Two(d.min);
  }

string ZoneTag() { return InpTzDisp == TM_Z_NY ? "NY" : (InpTzDisp == TM_Z_UTC ? "UTC" : (InpTzDisp == TM_Z_LONDON ? "LON" : "")); }


//+------------------------------------------------------------------+
//| generic helpers                                                  |
//+------------------------------------------------------------------+
bool IsNa(const double v) { return v == TM_NA; }
int BarSec() { return PeriodSeconds(_Period); }

datetime TimeAt(const int k)
  {
   if(k < 0) return g_t[0];
   if(k <= g_n - 1) return g_t[k];
   return g_t[g_n - 1] + (datetime)((k - (g_n - 1)) * BarSec());
  }

color Tint(const color c, const double alpha)   // blend with the chart background (MT objects have no transparency)
  {
   int cr = (int)(c & 0xFF), cg = (int)((c >> 8) & 0xFF), cb = (int)((c >> 16) & 0xFF);
   int br = (int)(g_bg & 0xFF), bgc = (int)((g_bg >> 8) & 0xFF), bb = (int)((g_bg >> 16) & 0xFF);
   int r = (int)(cr * alpha + br * (1.0 - alpha));
   int g = (int)(cg * alpha + bgc * (1.0 - alpha));
   int b = (int)(cb * alpha + bb * (1.0 - alpha));
   return (color)(r | (g << 8) | (b << 16));
  }

string Fp(const double p) { return DoubleToString(p, _Digits); }

string NextName(const string kind)
  {
   g_cnt++;
   return PFX + kind + IntegerToString(g_cnt);
  }

string MkLine(const string kind, const datetime t1, const double p1, const datetime t2, const double p2, const color c, const int style, const int width)
  {
   string n = NextName(kind);
   ObjectCreate(0, n, OBJ_TREND, 0, t1, p1, t2, p2);
   ObjectSetInteger(0, n, OBJPROP_COLOR, c);
   ObjectSetInteger(0, n, OBJPROP_STYLE, style);
   ObjectSetInteger(0, n, OBJPROP_WIDTH, width);
   ObjectSetInteger(0, n, OBJPROP_RAY_RIGHT, false);
   ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
   return n;
  }

string MkText(const string kind, const datetime t, const double p, const string text, const color c, const int anchor)
  {
   string n = NextName(kind);
   ObjectCreate(0, n, OBJ_TEXT, 0, t, p);
   ObjectSetString(0, n, OBJPROP_TEXT, text);
   ObjectSetInteger(0, n, OBJPROP_COLOR, c);
   ObjectSetInteger(0, n, OBJPROP_FONTSIZE, InpFont);
   ObjectSetInteger(0, n, OBJPROP_ANCHOR, anchor);
   ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
   return n;
  }

string MkRect(const string kind, const datetime t1, const double top, const datetime t2, const double bottom, const color col, const bool fill)
  {
   string n = NextName(kind);
   ObjectCreate(0, n, OBJ_RECTANGLE, 0, t1, top, t2, bottom);
   ObjectSetInteger(0, n, OBJPROP_COLOR, col);
   ObjectSetInteger(0, n, OBJPROP_FILL, fill);
   ObjectSetInteger(0, n, OBJPROP_BACK, true);
   ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
   return n;
  }


//+------------------------------------------------------------------+
//| input parsing, symbol type                                       |
//+------------------------------------------------------------------+
void ParseRange(const string s, const int defA, const int defB, int &a, int &b)
  {
   a = defA; b = defB;
   if(StringLen(s) < 9 || StringSubstr(s, 4, 1) != "-") return;
   int a1 = (int)StringToInteger(StringSubstr(s, 0, 2)), a2 = (int)StringToInteger(StringSubstr(s, 2, 2));
   int b1 = (int)StringToInteger(StringSubstr(s, 5, 2)), b2 = (int)StringToInteger(StringSubstr(s, 7, 2));
   if(a1 < 0 || a1 > 24 || b1 < 0 || b1 > 24 || a2 < 0 || a2 > 59 || b2 < 0 || b2 > 59) return;
   a = a1 * 60 + a2;
   b = b1 * 60 + b2;
   if(b == 1440) b = 0;
  }

bool HasAny(const string s, const string list)    // list separated by ","
  {
   int p = 0;
   while(p < StringLen(list))
     {
      int q = StringFind(list, ",", p);
      if(q < 0) q = StringLen(list);
      string w = StringSubstr(list, p, q - p);
      if(StringLen(w) > 0 && StringFind(s, w) >= 0) return true;
      p = q + 1;
     }
   return false;
  }

void DetectMarket()
  {
   string s = _Symbol;
   StringToUpper(s);
   bool crypto = HasAny(s, "BTC,ETH,XRP,LTC,DOGE,SOL,ADA,BNB,DOT,AVAX,MATIC,TRX,LINK,SHIB");
   bool forex = false;
   if(!crypto && StringLen(s) >= 6)
     {
      string cur = "USD,EUR,GBP,JPY,CHF,CAD,AUD,NZD";
      string a = StringSubstr(s, 0, 3), b = StringSubstr(s, 3, 3);
      forex = (StringFind(cur, a) >= 0 && StringFind(cur, b) >= 0);
     }
   g_isCrypto = (InpMarket == TM_M_CRYPTO) || (InpMarket == TM_M_AUTO && crypto);
   g_isForex  = (InpMarket == TM_M_FOREX)  || (InpMarket == TM_M_AUTO && forex);
  }

//+------------------------------------------------------------------+
//| series: ATR (RMA), New York clock, flags                         |
//+------------------------------------------------------------------+
bool InSess(const int m, const int a, const int b)
  {
   if(a < b) return m >= a && m < b;
   return m >= a || m < b;
  }


//+------------------------------------------------------------------+
//| markers (Raid / Break? / Run / Judas / SMT)                      |
//+------------------------------------------------------------------+
int AddMark(const int bar, const double price, const string text, const color c, const int dir, const int kind)
  {
   double off = 0.0;
   if(kind == 0)                                 // stack ordinary markers that sit close together
     {
      int lvl = 0;
      for(int k = 0; k < g_nMk; k++) if(g_mk[k].alive && g_mk[k].kind == 0 && MathAbs(g_mk[k].bar - bar) <= 8) lvl++;
      off = g_atr[MathMin(bar, g_n - 1)] * 0.5 * MathMin(lvl, 1) * (dir == 1 ? 1 : -1);
     }
   ArrayResize(g_mk, g_nMk + 1);
   g_mkId++;
   g_mk[g_nMk].id = g_mkId; g_mk[g_nMk].bar = bar; g_mk[g_nMk].price = price + off; g_mk[g_nMk].text = text;
   g_mk[g_nMk].col = c; g_mk[g_nMk].dir = dir; g_mk[g_nMk].kind = kind; g_mk[g_nMk].alive = true;
   g_nMk++;
   int cap = (kind == 0) ? 24 : EMaxLiq + 10;     // keep the newest ones only
   int cnt = 0;
   for(int k = 0; k < g_nMk; k++) if(g_mk[k].alive && g_mk[k].kind == kind) cnt++;
   for(int k = 0; k < g_nMk && cnt > cap; k++)
      if(g_mk[k].alive && g_mk[k].kind == kind) { g_mk[k].alive = false; cnt--; }
   return g_mkId;
  }

void KillMark(const int id)
  {
   for(int k = 0; k < g_nMk; k++) if(g_mk[k].id == id) g_mk[k].alive = false;
  }

void SetMarkText(const int id, const string text)
  {
   for(int k = 0; k < g_nMk; k++) if(g_mk[k].id == id) g_mk[k].text = text;
  }

//+------------------------------------------------------------------+
//| liquidity levels                                                 |
//+------------------------------------------------------------------+
void RemoveLq(const int idx)
  {
   for(int j = idx; j < g_nLq - 1; j++) g_lq[j] = g_lq[j + 1];
   g_nLq--;
  }

void AddLiq(const double px, const string nm, const int d, const int kind, const int ob, const int i)
  {
   if(IsNa(px)) return;
   for(int k = g_nLq - 1; k >= 0; k--)
      if(g_lq[k].name == nm)
        {
         if(g_lq[k].pend && g_lq[k].mk >= 0) KillMark(g_lq[k].mk);
         RemoveLq(k);
        }
   while(g_nLq >= EMaxLiq)                      // make room: the oldest level NOT waiting for its verdict goes first
     {
      int idx = -1;
      for(int k = 0; k < g_nLq && idx < 0; k++) if(!g_lq[k].pend) idx = k;
      if(idx < 0) break;
      RemoveLq(idx);
     }
   ArrayResize(g_lq, g_nLq + 1);
   g_lq[g_nLq].price = px; g_lq[g_nLq].name = nm; g_lq[g_nLq].dir = d; g_lq[g_nLq].kind = kind; g_lq[g_nLq].bar = i; g_lq[g_nLq].ob = ob;
   g_lq[g_nLq].swept = false; g_lq[g_nLq].sweptBar = 0; g_lq[g_nLq].pend = false; g_lq[g_nLq].hitBar = 0; g_lq[g_nLq].hitExt = 0.0; g_lq[g_nLq].mk = -1;
   g_nLq++;
  }

int KindScore(const int k) { return (k == 3 || k == 4) ? 4 : (k == 2 ? 3 : (k == 1 ? 2 : 1)); }

void ConfirmRaid(const int qi, const double ext, const int i)
  {
   int sc = KindScore(g_lq[qi].kind);
   double pen = MathAbs(ext - g_lq[qi].price);
   bool sameBar = (g_raidScBar == i);
   bool better = !sameBar || sc > g_raidSc || (sc == g_raidSc && pen > g_raidPen);
   if(g_lq[qi].mk >= 0) { KillMark(g_lq[qi].mk); g_lq[qi].mk = -1; }
   if(!better) return;
   if(sameBar && g_raidMk >= 0) { KillMark(g_raidMk); g_raidMk = -1; }
   g_raidDir = (g_lq[qi].dir == 1) ? -1 : 1;
   g_raidBar = i; g_raidT = g_t[i]; g_raidPx = ext; g_raidName = g_lq[qi].name; g_raidRej = true;
   g_raidSc = sc; g_raidPen = pen; g_raidScBar = i; g_evRaid = g_raidDir;
   if(InpRaidLbl) g_raidMk = AddMark(i, g_lq[qi].dir == 1 ? g_h[i] : g_l[i], "Raid " + g_lq[qi].name, C_ORANGE, g_lq[qi].dir == 1 ? 1 : -1, 1);
  }

void ChkLiq(const int i)
  {
   for(int k = g_nLq - 1; k >= 0; k--)
     {
      if(!g_lq[k].swept)
        {
         if(i > g_lq[k].bar)
           {
            bool hit = g_lq[k].dir == 1 ? g_h[i] > g_lq[k].price : g_l[i] < g_lq[k].price;
            if(hit)
              {
               g_lq[k].swept = true; g_lq[k].sweptBar = i;
               bool rej = g_lq[k].dir == 1 ? g_c[i] < g_lq[k].price : g_c[i] > g_lq[k].price;
               double ext = g_lq[k].dir == 1 ? g_h[i] : g_l[i];
               if(rej) ConfirmRaid(k, ext, i);
               else
                 {
                  g_lq[k].pend = true; g_lq[k].hitBar = i; g_lq[k].hitExt = ext;      // PENDING: back through = RAID, still beyond = RUN
                  if(InpRaidLbl) g_lq[k].mk = AddMark(i, ext, "Break? " + g_lq[k].name, C_GRAY, g_lq[k].dir == 1 ? 1 : -1, 1);
                 }
              }
           }
        }
      else
        {
         if(g_lq[k].pend)
           {
            g_lq[k].hitExt = g_lq[k].dir == 1 ? MathMax(g_lq[k].hitExt, g_h[i]) : MathMin(g_lq[k].hitExt, g_l[i]);
            bool back = g_lq[k].dir == 1 ? g_c[i] < g_lq[k].price : g_c[i] > g_lq[k].price;
            if(back)
              {
               g_lq[k].pend = false;
               ConfirmRaid(k, g_lq[k].hitExt, i);
              }
            else if(i - g_lq[k].hitBar >= InpPendBars)
              {
               g_lq[k].pend = false;
               g_evRun = g_lq[k].dir;
               if(g_lq[k].mk >= 0) SetMarkText(g_lq[k].mk, "Run " + g_lq[k].name);
              }
           }
         if(!g_lq[k].pend && i - g_lq[k].sweptBar > EKeepSwept) RemoveLq(k);
        }
     }
   if(g_raidRej && i - g_raidBar <= InpMssWin)
     {
      if(g_raidDir == 1)  g_raidPx = MathMin(IsNa(g_raidPx) ? g_l[i] : g_raidPx, g_l[i]);
      if(g_raidDir == -1) g_raidPx = MathMax(IsNa(g_raidPx) ? g_h[i] : g_raidPx, g_h[i]);
     }
  }


bool IsSunday(const int i) { return !g_isCrypto && g_dow[i] == 1; }

bool NewWeek(const int i)
  {
   if(i <= 0) return false;
   int a = g_isCrypto ? (g_dow[i] + 5) % 7 : g_dow[i] - 1;
   int b = g_isCrypto ? (g_dow[i - 1] + 5) % 7 : g_dow[i - 1] - 1;
   return a < b;
  }

void PeriodsBar(const int i)
  {
   bool newDay = i > 0 && g_day[i] != g_day[i - 1] && !IsSunday(i);
   bool newWk  = NewWeek(i);
   bool newMth = i > 0 && g_mon[i] != g_mon[i - 1];
   if(newDay)
     {
      if(!IsNa(g_curDH) && InpPdOn)
        {
         AddLiq(g_curDH, "PDH", 1, 2, g_curDHB, i);
         AddLiq(g_curDL, "PDL", -1, 2, g_curDLB, i);
        }
      g_curDH = g_h[i]; g_curDL = g_l[i]; g_curDHB = i; g_curDLB = i;
     }
   else if(!IsSunday(i))
     {
      if(IsNa(g_curDH) || g_h[i] > g_curDH) { g_curDH = g_h[i]; g_curDHB = i; }
      if(IsNa(g_curDL) || g_l[i] < g_curDL) { g_curDL = g_l[i]; g_curDLB = i; }
     }
   if(newWk)
     {
      if(!IsNa(g_curWH) && InpPwOn)
        {
         AddLiq(g_curWH, "PWH", 1, 3, g_curWHB, i);
         AddLiq(g_curWL, "PWL", -1, 3, g_curWLB, i);
        }
      g_curWH = g_h[i]; g_curWL = g_l[i]; g_curWHB = i; g_curWLB = i;
     }
   else
     {
      if(IsNa(g_curWH) || g_h[i] > g_curWH) { g_curWH = g_h[i]; g_curWHB = i; }
      if(IsNa(g_curWL) || g_l[i] < g_curWL) { g_curWL = g_l[i]; g_curWLB = i; }
     }
   if(newMth)
     {
      if(!IsNa(g_curMH) && EPm)
        {
         AddLiq(g_curMH, "PMH", 1, 4, g_curMHB, i);
         AddLiq(g_curML, "PML", -1, 4, g_curMLB, i);
        }
      g_curMH = g_h[i]; g_curML = g_l[i]; g_curMHB = i; g_curMLB = i;
     }
   else
     {
      if(IsNa(g_curMH) || g_h[i] > g_curMH) { g_curMH = g_h[i]; g_curMHB = i; }
      if(IsNa(g_curML) || g_l[i] < g_curML) { g_curML = g_l[i]; g_curMLB = i; }
     }
  }


bool PivotHigh(const int c, const int len)
  {
   for(int k = 1; k <= len; k++) if(!(g_h[c] > g_h[c - k]) || !(g_h[c] > g_h[c + k])) return false;
   return true;
  }

bool PivotLow(const int c, const int len)
  {
   for(int k = 1; k <= len; k++) if(!(g_l[c] < g_l[c - k]) || !(g_l[c] < g_l[c + k])) return false;
   return true;
  }


int g_nItem = 0;
TmItem g_item[];
double g_lblP[];
int g_nLblP = 0;

void AddItem(const string text, const color c, const double price)
  {
   ArrayResize(g_item, g_nItem + 1);
   g_item[g_nItem].text = text; g_item[g_nItem].col = c; g_item[g_nItem].price = price;
   g_nItem++;
  }

double Rng150()
  {
   int s = MathMax(0, g_n - 2 - 149);
   double hi = -DBL_MAX, lo = DBL_MAX;
   for(int k = s; k <= g_n - 2; k++) { if(g_h[k] > hi) hi = g_h[k]; if(g_l[k] < lo) lo = g_l[k]; }
   return (hi > lo) ? hi - lo : 0.0;
  }

void FlushItems()                       // right-side labels: sorted by price, never closer than the minimum gap
  {
   g_nLblP = 0;
   if(g_nItem <= 0) return;
   int ord[];
   ArrayResize(ord, g_nItem);
   for(int k = 0; k < g_nItem; k++) ord[k] = k;
   for(int a = 1; a < g_nItem; a++)     // insertion sort, price descending
     {
      int v = ord[a], b = a - 1;
      while(b >= 0 && g_item[ord[b]].price < g_item[v].price) { ord[b + 1] = ord[b]; b--; }
      ord[b + 1] = v;
     }
   double gap = Rng150() * InpLblGap / 100.0;
   double prevY = TM_NA;
   datetime tx = TimeAt(g_n - 1 + InpLblPad);
   for(int k = 0; k < g_nItem; k++)
     {
      int ix = ord[k];
      double y = g_item[ix].price;
      if(!IsNa(prevY) && prevY - y < gap) y = prevY - gap;
      MkText("RL", tx, y, g_item[ix].text, g_item[ix].col, ANCHOR_LEFT);
      ArrayResize(g_lblP, g_nLblP + 1); g_lblP[g_nLblP] = y; g_nLblP++;
      prevY = y;
     }
   g_nItem = 0;
  }


const int PN_ROWS = 8, PN_M = 14;
int g_pnW = 430, g_pnRH = 18, g_pnKeyW = 150;
string g_pk[10], g_pv[10];
color  g_pc[10];

int  g_pnCorner = CORNER_LEFT_UPPER;
long g_pnSig = -1;

int PnH() { return PN_ROWS * g_pnRH + 12; }

int WmHeight() { return InpShowWm ? InpWmBottom + (int)(InpWmSize * 2.3) + 6 : 0; }   // bottom-left space used by the watermark

int PnMarginY(const int corner)           // vertical gap from the chart edge; bottom-left leaves room for the watermark
  {
   if(corner == CORNER_LEFT_LOWER) return PN_M + WmHeight();
   return PN_M;
  }

int FixedCorner()
  {
   if(InpPanelPos == TM_C_TOPLEFT) return CORNER_LEFT_UPPER;
   if(InpPanelPos == TM_C_BOTTOMRIGHT) return CORNER_RIGHT_LOWER;
   if(InpPanelPos == TM_C_BOTTOMLEFT) return CORNER_LEFT_LOWER;
   return CORNER_RIGHT_UPPER;
  }

// how much of the visible chart a panel in this corner would cover (bars + right-hand price labels)
double CornerCover(const int corner, const int W, const int H, const int first, const int vis, const double pmax, const double pmin, const double ppb)
  {
   bool right = (corner == CORNER_RIGHT_UPPER || corner == CORNER_RIGHT_LOWER);
   bool bottom = (corner == CORNER_LEFT_LOWER || corner == CORNER_RIGHT_LOWER);
   int x0 = right ? W - PN_M - g_pnW : PN_M, x1 = x0 + g_pnW;
   int my = PnMarginY(corner);
   int y0 = bottom ? H - my - PnH() : my, y1 = y0 + PnH();
   double sc = 0.0, k = (double)H / (pmax - pmin);
   for(int s = first; s > first - vis && s >= 0; s--)
     {
      int i = g_n - 1 - s;
      if(i < 0 || i >= g_n) continue;
      double bx = (first - s + 0.5) * ppb;
      if(bx < x0 - 2 || bx > x1 + 2) continue;
      double yt = (pmax - g_h[i]) * k, yb = (pmax - g_l[i]) * k;
      if(yb >= y0 && yt <= y1) sc += 1.0;
     }
   if(right)                                   // right-hand level labels live under the right corners
      for(int j = 0; j < g_nLblP; j++)
        {
         double yy = (pmax - g_lblP[j]) * k;
         if(yy >= y0 - 6 && yy <= y1 + 6) sc += 6.0;
        }
   return sc;
  }

int PanelCorner()
  {
   if(InpPanelPos != TM_C_AUTO) return FixedCorner();
   return g_pnCorner;
  }

void ChooseCorner()                         // Auto: pick the emptiest corner, with hysteresis so it does not jump around
  {
   if(InpPanelPos != TM_C_AUTO) { g_pnCorner = FixedCorner(); return; }
   int W = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), H = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   int first = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR), vis = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   double pmax = ChartGetDouble(0, CHART_PRICE_MAX, 0), pmin = ChartGetDouble(0, CHART_PRICE_MIN, 0);
   if(W <= 0 || H <= 0 || vis <= 0 || pmax <= pmin) return;
   long sig = (long)W * 1000003L + H * 10007L + first * 101L + vis + (long)(pmax * 1000.0) % 9973 + g_nLblP * 13L + g_n;
   if(sig == g_pnSig) return;
   g_pnSig = sig;
   double shiftPct = (InpRightMargin > 0) ? (double)MathMin(InpRightMargin, 50) / 100.0 : 0.0;
   double ppb = (double)W * (1.0 - shiftPct) / vis;
   int cs[4];
   cs[0] = CORNER_LEFT_UPPER; cs[1] = CORNER_RIGHT_UPPER; cs[2] = CORNER_LEFT_LOWER; cs[3] = CORNER_RIGHT_LOWER;
   int best = -1; double bs = 1e18, cur = 1e18;
   for(int k = 0; k < 4; k++)
     {
      double sc = CornerCover(cs[k], W, H, first, vis, pmax, pmin, ppb);
      if(cs[k] == g_pnCorner) cur = sc;
      if(sc < bs) { bs = sc; best = cs[k]; }
     }
   if(best >= 0 && cur > bs + 3.0) g_pnCorner = best;      // move only when clearly better
  }

void PanelRow(const int row, const string key, const string val, const color vc)
  {
   g_pk[row] = key; g_pv[row] = (val == "" ? " " : val); g_pc[row] = vc;      // empty text would show the default word "Label"
  }

void PanelMeasure()                         // size the panel from the real text widths (works at any screen scaling)
  {
   TextSetFont("Consolas", -InpFont * 10);
   uint w = 0, h = 0;
   int kw = 0, vw = 0, rh = 0;
   for(int r = 0; r < PN_ROWS; r++)
     {
      TextGetSize(g_pk[r], w, h); if((int)w > kw) kw = (int)w; if((int)h > rh) rh = (int)h;
      TextGetSize(g_pv[r], w, h); if((int)w > vw) vw = (int)w;
     }
   if(kw <= 0 || rh <= 0)                   // measuring not available: rough estimate
     { kw = (int)(InpFont * 8.0 * 20); rh = InpFont * 2; vw = (int)(InpFont * 8.0 * 30); }
   kw = (int)(kw * 1.12); vw = (int)(vw * 1.12);          // safety margin: fonts differ between PCs
   g_pnKeyW = kw + 16;
   g_pnRH = MathMax(18, rh + 4);
   g_pnW = MathMax(260, 10 + g_pnKeyW + vw + 28);
  }

void PanelOrigin(int &x0, int &y0)          // top-left pixel of the panel; every panel object is anchored top-left, so right/bottom corners are computed from the chart size
  {
   int corner = PanelCorner();
   int W = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), H = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   bool right = (corner == CORNER_RIGHT_UPPER || corner == CORNER_RIGHT_LOWER);
   bool bottom = (corner == CORNER_LEFT_LOWER || corner == CORNER_RIGHT_LOWER);
   x0 = right ? W - PN_M - g_pnW : PN_M;
   y0 = bottom ? H - PnMarginY(corner) - PnH() : PnMarginY(corner);
   if(x0 < 0) x0 = 0;
   if(y0 < 0) y0 = 0;
  }

void PanelDrawRow(const int row, const int x0, const int y0)
  {
   int yy = y0 + 6 + row * g_pnRH;
   for(int k = 0; k < 2; k++)
     {
      string n = PFX + "PN" + IntegerToString(row) + "_" + IntegerToString(k);
      if(ObjectFind(0, n) < 0) ObjectCreate(0, n, OBJ_LABEL, 0, 0, 0);
      int xin = (k == 0) ? 10 : 10 + g_pnKeyW;               // x inside the panel
      ObjectSetInteger(0, n, OBJPROP_CORNER, CORNER_LEFT_UPPER);
      ObjectSetInteger(0, n, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
      ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x0 + xin);
      ObjectSetInteger(0, n, OBJPROP_YDISTANCE, yy);
      ObjectSetInteger(0, n, OBJPROP_COLOR, k == 0 ? (row == 0 ? BRAND_GREEN : C_GRAY) : g_pc[row]);
      ObjectSetInteger(0, n, OBJPROP_FONTSIZE, InpFont);
      ObjectSetString(0, n, OBJPROP_FONT, "Consolas");
      ObjectSetString(0, n, OBJPROP_TEXT, k == 0 ? g_pk[row] : g_pv[row]);
      ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, n, OBJPROP_ZORDER, 10);
     }
  }

void PanelBack(const int x0, const int y0)
  {
   string n = PFX + "PNBG";
   if(ObjectFind(0, n) < 0) ObjectCreate(0, n, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, n, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x0);
   ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y0);
   ObjectSetInteger(0, n, OBJPROP_XSIZE, g_pnW);
   ObjectSetInteger(0, n, OBJPROP_YSIZE, PnH());
   ObjectSetInteger(0, n, OBJPROP_BGCOLOR, Tint(BRAND_DARK, 0.92));
   ObjectSetInteger(0, n, OBJPROP_COLOR, Tint(BRAND_GREEN, 0.5));
   ObjectSetInteger(0, n, OBJPROP_BORDER_TYPE, 1);
   ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, n, OBJPROP_BACK, false);
   ObjectSetInteger(0, n, OBJPROP_ZORDER, 9);
  }


void PaintWatermark()
  {
   if(!InpShowWm)
     {
      ObjectDelete(0, PFX + "WM1");
      ObjectDelete(0, PFX + "WM2");
      return;
     }
   long bg = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   int r = (int)(bg & 0xFF), g = (int)((bg >> 8) & 0xFF), b = (int)((bg >> 16) & 0xFF);
   bool light = (r * 299 + g * 587 + b * 114) / 1000 > 128;
   color orgCol = light ? C'30,34,41' : clrWhite;
   TextSetFont("Arial Black", -InpWmSize * 10);            // measure the real text width so the two parts never overlap
   uint tw = 0, th = 0;
   TextGetSize("crypex", tw, th);
   int wCrypex = (tw > 0) ? (int)tw + 4 : (int)(InpWmSize * 7.0);
   string n1 = PFX + "WM1", n2 = PFX + "WM2";
   for(int k = 0; k < 2; k++)
     {
      string n = (k == 0) ? n1 : n2;
      if(ObjectFind(0, n) < 0) ObjectCreate(0, n, OBJ_LABEL, 0, 0, 0);
      ObjectSetInteger(0, n, OBJPROP_CORNER, CORNER_LEFT_LOWER);
      ObjectSetInteger(0, n, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
      ObjectSetInteger(0, n, OBJPROP_XDISTANCE, InpWmGap + (k == 0 ? 0 : wCrypex));
      ObjectSetInteger(0, n, OBJPROP_YDISTANCE, InpWmBottom);
      ObjectSetInteger(0, n, OBJPROP_COLOR, k == 0 ? BRAND_GREEN : orgCol);
      ObjectSetInteger(0, n, OBJPROP_FONTSIZE, InpWmSize);
      ObjectSetString(0, n, OBJPROP_FONT, "Arial Black");
      ObjectSetString(0, n, OBJPROP_TEXT, k == 0 ? "crypex" : ".org");
      ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, n, OBJPROP_ZORDER, 10);
     }
  }


void Notify(const string msg)
  {
   string full = "Crypex ICT Models " + _Symbol + ": " + msg;
   if(InpAlertPopup) Alert(full);
   if(InpAlertPush)  SendNotification(full);
   if(InpAlertMail)  SendMail("Crypex ICT Models", full);
  }


//+------------------------------------------------------------------+
//| Crypex ICT Models: settings resolved from the Preset             |
//+------------------------------------------------------------------+
void ResolvePreset()
  {
   int p = (int)InpPreset;
   if(p == MD_P_CUSTOM)
     {
      EDispMark = InpDispMark; EShowBos = InpShowBos; EIfvg = InpIfvgOn; EVi = InpViOn; EBpr = InpBprOn; EMit = InpMitOn; ERb = InpRbOn;
      EPb = InpPbOn; EVac = InpVacOn; EZoneMt = InpZoneMt; EPdZone = InpPdZoneOn; ESessLvl = InpSessLvlOn; EPm = InpPmOn;
      EQual = InpFvgQual; EStats = InpSigOn && InpShowStats;
      EFvgKeep = InpFvgKeep; EZoneMax = InpZoneMax; EMaxLiq = InpMaxLiq; EKeepSwept = InpKeepSwept; EStructKeep = InpStructKeep;
      ELblNear = InpLblNear; EFvgMin = InpFvgMin;
      return;
     }
   bool cl = (p == MD_P_CLEAN), fu = (p == MD_P_FULL);
   EDispMark = !cl; EShowBos = !cl; EIfvg = !cl; EVi = !cl; EBpr = !cl; EMit = !cl; ERb = !cl; EPb = !cl; EVac = !cl;
   EZoneMt = !cl; EPdZone = !cl; ESessLvl = !cl; EPm = fu; EQual = cl; EStats = InpSigOn && fu;
   EFvgKeep = cl ? 5 : (fu ? 30 : 16);
   EZoneMax = cl ? 6 : (fu ? 40 : 24);
   EMaxLiq = cl ? 12 : (fu ? 60 : 40);
   EKeepSwept = cl ? 20 : (fu ? 100 : 60);
   EStructKeep = cl ? 6 : (fu ? 24 : 14);
   ELblNear = cl ? 0.25 : (fu ? 0.6 : 0.3);
   EFvgMin = cl ? 0.3 : (fu ? 0.05 : 0.1);
  }

//+------------------------------------------------------------------+
//| model structures                                                 |
//+------------------------------------------------------------------+
struct TmSessM   { string name; int a; int b; bool on; color col; bool active; double hi; double lo; int hiBar; int loBar;
                  double fHi; double fLo; int fHiBar; int fLoBar; };
struct TmFvg    { double top; double bot; int dir; int bar; int state; double ext; bool uni; bool bp; string nm; };
struct TmZone   { double top; double bot; int dir; int kind; string nm; int bar; int born; bool touched; bool prop; bool uni; };
struct TmSetup  { int model; int dir; double entry; double sl; double tp; int born; int fillBar; int state; int score; double rr; double cost; int finBar; bool win; };
struct TmStruct { double px; int fromBar; int toBar; string txt; color col; };
struct TmMBox    { int b0; int b1; double top; double bot; string txt; color col; };
struct TmDisp   { int bar; int dir; };

TmSessM   g_ms[5];
TmFvg    g_fvg[];    int g_nFvg = 0;
TmZone   g_zn[];     int g_nZn = 0;
TmSetup  g_su[];     int g_nSu = 0;
TmStruct g_st[];     int g_nSt = 0;
TmMBox    g_mb[];     int g_nMb = 0;
TmDisp   g_dp[];     int g_nDp = 0;
int      g_htf[];

int      g_trend = 0, g_mssDir = 0, g_mssBar = -9999;
datetime g_mssT = 0;
bool     g_swHiX = true, g_swLoX = true;
int      g_dispBullBar = -9999, g_dispBearBar = -9999;
double   g_eq = TM_NA, g_rngTop = TM_NA, g_rngBot = TM_NA, g_oteTop = TM_NA, g_oteBot = TM_NA, g_oteMid = TM_NA, g_oteSl = TM_NA, g_oteTp = TM_NA;
int      g_oteDir = 0;
bool     g_oteFired = false, g_dealOk = false;
int      g_w22Dir = 0, g_w22Bar = 0;
double   g_w22Px = TM_NA;
bool     g_sbFired = false;
int      g_evMss = 0, g_evBrk = 0, g_evFvg = 0, g_evUni = 0, g_evSig = 0;
double   g_nfTop = TM_NA, g_nfBot = TM_NA, g_nfExt = TM_NA;
int      g_nfDir = 0;
double   g_uniTop = TM_NA, g_uniBot = TM_NA, g_uniFar = TM_NA;
int      g_uniDir = 0, g_uniBar = -9999;
bool     g_uniDone = true;
int      g_capRej = 0;
int      g_win[5], g_loss[5];
int      g_sbA[3], g_sbB[3];

string Bolt() { return ShortToString(0x26A1); }
long   AgeSec() { return (long)InpAgeMin * 60; }
int    Cl1400(const int x) { return MathMax(x, g_n - 1 - 1400); }

//+------------------------------------------------------------------+
//| sessions (high / low tracking only; finished ones become levels) |
//+------------------------------------------------------------------+
void InitModels()
  {
   ResolvePreset();
   string nm[5] = {"Asia", "London KZ", "NY AM KZ", "London Close KZ", "NY PM KZ"};
   int    dA[5] = {1200, 120, 420, 600, 810};
   int    dB[5] = {0, 300, 600, 720, 960};
   string rg[5]; bool on[5]; color cl[5];
   rg[0] = InpAsiaR; rg[1] = InpLonR; rg[2] = InpNyaR; rg[3] = InpLcR; rg[4] = InpNypR;
   on[0] = InpAsiaOn; on[1] = InpLonOn; on[2] = InpNyaOn; on[3] = InpLcOn; on[4] = InpNypOn;
   cl[0] = C_PURPLE; cl[1] = C_BLUE; cl[2] = C_BULL; cl[3] = C_ORANGE; cl[4] = C_PINK;
   for(int k = 0; k < 5; k++)
     {
      g_ms[k].name = nm[k]; g_ms[k].on = on[k]; g_ms[k].col = cl[k];
      ParseRange(rg[k], dA[k], dB[k], g_ms[k].a, g_ms[k].b);
     }
   ParseRange(InpSb1R, 180, 240, g_sbA[0], g_sbB[0]);
   ParseRange(InpSb2R, 600, 660, g_sbA[1], g_sbB[1]);
   ParseRange(InpSb3R, 840, 900, g_sbA[2], g_sbB[2]);
  }

bool Fl(const int i, const int bit) { return i >= 0 && (g_fl[i] & (1 << bit)) != 0; }
bool SbAny(const int i)  { return InpSbOn && (Fl(i, 5) || Fl(i, 6) || Fl(i, 7)); }
bool KzAny(const int i)  { return (InpLonOn && Fl(i, 1)) || (InpNyaOn && Fl(i, 2)) || (InpLcOn && Fl(i, 3)) || (InpNypOn && Fl(i, 4)) || SbAny(i); }

bool SessUpd(const int k, const bool inS, const int i)
  {
   bool ended = false;
   if(!g_ms[k].on) return false;
   if(inS)
     {
      if(!g_ms[k].active)
        { g_ms[k].active = true; g_ms[k].hi = g_h[i]; g_ms[k].lo = g_l[i]; g_ms[k].hiBar = i; g_ms[k].loBar = i; }
      else
        {
         if(g_h[i] > g_ms[k].hi) { g_ms[k].hi = g_h[i]; g_ms[k].hiBar = i; }
         if(g_l[i] < g_ms[k].lo) { g_ms[k].lo = g_l[i]; g_ms[k].loBar = i; }
        }
     }
   else if(g_ms[k].active)
     {
      g_ms[k].active = false; ended = true;
      g_ms[k].fHi = g_ms[k].hi; g_ms[k].fLo = g_ms[k].lo; g_ms[k].fHiBar = g_ms[k].hiBar; g_ms[k].fLoBar = g_ms[k].loBar;
     }
   return ended;
  }

void SessEnd(const int k, const int i)
  {
   if(!ESessLvl) return;
   AddLiq(g_ms[k].fHi, g_ms[k].name + " H", 1, 1, g_ms[k].fHiBar, i);
   AddLiq(g_ms[k].fLo, g_ms[k].name + " L", -1, 1, g_ms[k].fLoBar, i);
  }

//+------------------------------------------------------------------+
//| series: ATR, New York clock, session flags, HTF bias             |
//+------------------------------------------------------------------+
void BuildHtf()
  {
   for(int i = 0; i < g_n; i++) g_htf[i] = 0;
   string sym = _Symbol;
   if(PeriodSeconds(InpBiasTF) <= PeriodSeconds(_Period)) return;        // the bias must come from a higher timeframe
   int kOld = iBarShift(sym, InpBiasTF, g_t[0], false);
   if(kOld < 0) return;
   int kMax = kOld + 120;
   while(kMax > 0 && iClose(sym, InpBiasTF, kMax) <= 0.0) kMax--;
   if(kMax < 2) return;
   double ema[];
   ArrayResize(ema, kMax + 1);
   double a = 2.0 / (InpBiasLen + 1.0);
   for(int k = kMax; k >= 0; k--)
     {
      double c = iClose(sym, InpBiasTF, k);
      ema[k] = (k == kMax) ? c : a * c + (1.0 - a) * ema[k + 1];
     }
   for(int i = 0; i < g_n; i++)                                           // previous COMPLETED higher-timeframe bar only: cannot repaint
     {
      int k = iBarShift(sym, InpBiasTF, g_t[i], false);
      if(k < 0) continue;
      int p = k + 1;
      if(p > kMax) continue;
      g_htf[i] = (iClose(sym, InpBiasTF, p) > ema[p]) ? 1 : -1;
     }
  }

void BuildSeries()
  {
   ArrayResize(g_atr, g_n); ArrayResize(g_ny, g_n); ArrayResize(g_mn, g_n); ArrayResize(g_dow, g_n);
   ArrayResize(g_day, g_n); ArrayResize(g_mon, g_n); ArrayResize(g_fl, g_n); ArrayResize(g_htf, g_n);
   double sum = 0.0;
   for(int i = 0; i < g_n; i++)
     {
      double tr = g_h[i] - g_l[i];
      if(i > 0) tr = MathMax(tr, MathMax(MathAbs(g_h[i] - g_c[i - 1]), MathAbs(g_l[i] - g_c[i - 1])));
      if(i < 13) { sum += tr; g_atr[i] = 0.0; }
      else if(i == 13) { sum += tr; g_atr[i] = sum / 14.0; }
      else g_atr[i] = (g_atr[i - 1] * 13.0 + tr) / 14.0;
     }
   for(int i = 0; i < g_n; i++)
     {
      datetime u = ServerToUtc(g_t[i]);
      datetime ny = ToNY(u);
      g_ny[i] = ny;
      MqlDateTime d; TimeToStruct(ny, d);
      g_mn[i] = d.hour * 60 + d.min;
      g_dow[i] = d.day_of_week + 1;
      g_day[i] = d.day;
      g_mon[i] = d.mon;
      int f = 0;
      if(g_intra)
        {
         int m = g_mn[i];
         for(int k = 0; k < 5; k++) if(InSess(m, g_ms[k].a, g_ms[k].b)) f |= (1 << k);
         for(int k = 0; k < 3; k++) if(InSess(m, g_sbA[k], g_sbB[k])) f |= (1 << (5 + k));
        }
      g_fl[i] = f;
     }
   BuildHtf();
  }

//+------------------------------------------------------------------+
//| state                                                            |
//+------------------------------------------------------------------+
void ResetState()
  {
   g_nMk = 0; g_mkId = 0; g_nLq = 0; g_cnt = 0;
   ArrayResize(g_mk, 0); ArrayResize(g_lq, 0);
   g_nFvg = 0; g_nZn = 0; g_nSu = 0; g_nSt = 0; g_nMb = 0; g_nDp = 0;
   ArrayResize(g_fvg, 0); ArrayResize(g_zn, 0); ArrayResize(g_su, 0); ArrayResize(g_st, 0); ArrayResize(g_mb, 0); ArrayResize(g_dp, 0);
   for(int k = 0; k < 5; k++)
     { g_ms[k].active = false; g_ms[k].fHi = TM_NA; g_ms[k].fLo = TM_NA; g_ms[k].hi = TM_NA; g_ms[k].lo = TM_NA; g_ms[k].hiBar = 0; g_ms[k].loBar = 0; g_ms[k].fHiBar = 0; g_ms[k].fLoBar = 0; }
   g_swHi = TM_NA; g_pSwHi = TM_NA; g_swLo = TM_NA; g_pSwLo = TM_NA; g_swHiBar = 0; g_swLoBar = 0;
   g_swHiX = true; g_swLoX = true; g_trend = 0; g_mssDir = 0; g_mssBar = -9999; g_mssT = 0;
   g_dispBullBar = -9999; g_dispBearBar = -9999;
   g_raidDir = 0; g_raidBar = -9999; g_raidT = 0; g_raidSc = 0; g_raidScBar = -1; g_raidMk = -1; g_raidPx = TM_NA; g_raidPen = 0.0; g_raidName = ""; g_raidRej = false;
   g_curDH = TM_NA; g_curDL = TM_NA; g_curWH = TM_NA; g_curWL = TM_NA; g_curMH = TM_NA; g_curML = TM_NA;
   g_curDHB = 0; g_curDLB = 0; g_curWHB = 0; g_curWLB = 0; g_curMHB = 0; g_curMLB = 0;
   g_jdDone = false; g_po3Bias = 0; g_po3DnBar = -9999; g_po3UpBar = -9999; g_po3DnExt = TM_NA; g_po3UpExt = TM_NA; g_moPx = TM_NA;
   g_eq = TM_NA; g_rngTop = TM_NA; g_rngBot = TM_NA; g_oteDir = 0; g_oteTop = TM_NA; g_oteBot = TM_NA; g_oteMid = TM_NA; g_oteSl = TM_NA; g_oteTp = TM_NA;
   g_oteFired = false; g_dealOk = false;
   g_w22Dir = 0; g_w22Bar = 0; g_w22Px = TM_NA; g_sbFired = false;
   g_nfTop = TM_NA; g_nfBot = TM_NA; g_nfExt = TM_NA; g_nfDir = 0;
   g_uniTop = TM_NA; g_uniBot = TM_NA; g_uniFar = TM_NA; g_uniDir = 0; g_uniBar = -9999; g_uniDone = true;
   g_capRej = 0;
   for(int k = 0; k < 5; k++) { g_win[k] = 0; g_loss[k] = 0; }
   g_evRaid = 0; g_evRun = 0; g_evMss = 0; g_evBrk = 0; g_evFvg = 0; g_evUni = 0; g_evSig = 0;
  }

//+------------------------------------------------------------------+
//| displacement                                                     |
//+------------------------------------------------------------------+
bool DispBull(const int i)
  {
   double rg = g_h[i] - g_l[i], bd = MathAbs(g_c[i] - g_o[i]);
   return g_c[i] > g_o[i] && rg > 0 && g_atr[i] > 0 && bd >= InpDispAtr * g_atr[i] && bd / rg >= InpDispBody;
  }
bool DispBear(const int i)
  {
   double rg = g_h[i] - g_l[i], bd = MathAbs(g_c[i] - g_o[i]);
   return g_c[i] < g_o[i] && rg > 0 && g_atr[i] > 0 && bd >= InpDispAtr * g_atr[i] && bd / rg >= InpDispBody;
  }
bool MidBull(const int k) { return k >= 0 && g_atr[k] > 0 && g_c[k] > g_o[k] && MathAbs(g_c[k] - g_o[k]) >= InpFvgMid * g_atr[k]; }
bool MidBear(const int k) { return k >= 0 && g_atr[k] > 0 && g_c[k] < g_o[k] && MathAbs(g_c[k] - g_o[k]) >= InpFvgMid * g_atr[k]; }

void AddDisp(const int bar, const int dir)
  {
   ArrayResize(g_dp, g_nDp + 1);
   g_dp[g_nDp].bar = bar; g_dp[g_nDp].dir = dir; g_nDp++;
   while(g_nDp > 12) { for(int j = 0; j < g_nDp - 1; j++) g_dp[j] = g_dp[j + 1]; g_nDp--; }
  }

//+------------------------------------------------------------------+
//| zones (blocks)                                                   |
//+------------------------------------------------------------------+
int AddZone(const double top, const double bot, const int d, const int kind, const int bar, const string nm, const int i)
  {
   bool ok = (kind == 1 && InpObOn) || (kind == 2 && InpBrkOn) || (kind == 3 && EMit) || (kind == 4 && ERb) || (kind == 5 && EPb) || (kind == 6 && EVac);
   if(!ok || !(top > bot)) return -1;
   for(int q = 0; q < g_nZn; q++)                   // the same candle can give the same block twice (repeated breaks of one swing): keep one
      if(g_zn[q].kind == kind && g_zn[q].dir == d && g_zn[q].bar == bar) return q;
   while(g_nZn >= EZoneMax) { for(int j = 0; j < g_nZn - 1; j++) g_zn[j] = g_zn[j + 1]; g_nZn--; }
   ArrayResize(g_zn, g_nZn + 1);
   g_zn[g_nZn].top = top; g_zn[g_nZn].bot = bot; g_zn[g_nZn].dir = d; g_zn[g_nZn].kind = kind; g_zn[g_nZn].nm = nm;
   g_zn[g_nZn].bar = bar; g_zn[g_nZn].born = i; g_zn[g_nZn].touched = false; g_zn[g_nZn].prop = false; g_zn[g_nZn].uni = false;
   g_nZn++;
   return g_nZn - 1;
  }

int FindOff(const int cBar, const bool wantBear, const int span, const int i)
  {
   int base = i - cBar, res = base;
   bool found = false;
   for(int k = 0; k <= span; k++)
     {
      int o = base + k;
      if(o <= 1000 && !found)
        {
         int idx = i - o;
         if(idx < 0) continue;
         bool isb = g_c[idx] < g_o[idx];
         if(isb == wantBear) { res = o; found = true; }
        }
     }
   return res;
  }

void BlockFromIdx(const int idx, const int d, double &t, double &b)
  {
   t = g_h[idx]; b = g_l[idx];
   if(InpZoneMode == MD_Z_BODY) { t = MathMax(g_o[idx], g_c[idx]); b = MathMin(g_o[idx], g_c[idx]); }
   if(InpZoneMode == MD_Z_OPEN)
     {
      if(d == 1) t = MathMax(g_o[idx], g_c[idx]);
      else b = MathMin(g_o[idx], g_c[idx]);
     }
  }

void ZoneAt(const int cBar, const bool wantBear, const int d, const int kind, const string nm, const int i)
  {
   if(i - cBar > 900) return;
   int o = FindOff(cBar, wantBear, 3, i);
   int idx = i - o;
   if(idx < 0) return;
   double t, b;
   BlockFromIdx(idx, d, t, b);
   AddZone(t, b, d, kind, idx, nm, i);
  }

void UpdZones(const int i)
  {
   double pqT[], pqB[]; int pqD[]; int np = 0;
   for(int k = g_nZn - 1; k >= 0; k--)
     {
      bool dead = false;
      if(g_zn[k].kind == 6) dead = (g_zn[k].dir == 1) ? g_l[i] <= g_zn[k].bot : g_h[i] >= g_zn[k].top;
      else dead = (g_zn[k].dir == 1) ? g_c[i] < g_zn[k].bot : g_c[i] > g_zn[k].top;
      if(dead)
        {
         for(int j = k; j < g_nZn - 1; j++) g_zn[j] = g_zn[j + 1];
         g_nZn--;
         continue;
        }
      if(!g_zn[k].touched)
        {
         bool tch = (g_zn[k].dir == 1) ? g_l[i] <= g_zn[k].top : g_h[i] >= g_zn[k].bot;
         if(tch && i > g_zn[k].bar) g_zn[k].touched = true;
        }
      if(EPb && !g_zn[k].prop && g_zn[k].kind >= 1 && g_zn[k].kind <= 3 && i > g_zn[k].bar)
        {
         if(g_zn[k].dir == 1 && g_c[i] > g_o[i] && g_l[i] <= g_zn[k].top && g_l[i] >= g_zn[k].bot && g_c[i] > g_zn[k].top)
           {
            g_zn[k].prop = true;
            ArrayResize(pqT, np + 1); ArrayResize(pqB, np + 1); ArrayResize(pqD, np + 1);
            pqT[np] = MathMin(g_o[i], g_zn[k].top); pqB[np] = g_l[i]; pqD[np] = 1; np++;
           }
         if(g_zn[k].dir == -1 && g_c[i] < g_o[i] && g_h[i] >= g_zn[k].bot && g_h[i] <= g_zn[k].top && g_c[i] < g_zn[k].bot)
           {
            g_zn[k].prop = true;
            ArrayResize(pqT, np + 1); ArrayResize(pqB, np + 1); ArrayResize(pqD, np + 1);
            pqT[np] = g_h[i]; pqB[np] = MathMax(g_o[i], g_zn[k].bot); pqD[np] = -1; np++;
           }
        }
     }
   for(int q = 0; q < np; q++)
      AddZone(pqT[q], pqB[q], pqD[q], 5, i, pqD[q] == 1 ? "Bull Propulsion" : "Bear Propulsion", i);
  }

void Gaps(const int i)
  {
   if(!EVac || i <= 0 || g_atr[i] <= 0) return;
   double g = g_o[i] - g_c[i - 1];
   if(MathAbs(g) < InpGapAtr * g_atr[i]) return;
   bool wkndGap = g_intra && (g_t[i] - g_t[i - 1]) >= 24 * 3600;
   bool dayChg = g_day[i] != g_day[i - 1];
   bool ndog = g_intra && g_mn[i] >= 1020 && g_mn[i - 1] < 1020 && !dayChg;
   string nm = wkndGap ? "NWOG" : (ndog ? "NDOG" : "Vacuum");
   AddZone(MathMax(g_o[i], g_c[i - 1]), MathMin(g_o[i], g_c[i - 1]), g > 0 ? 1 : -1, 6, i - 1, nm, i);
  }

//+------------------------------------------------------------------+
//| fair value gaps                                                  |
//+------------------------------------------------------------------+
void AddMisc(const int b0, const int b1, const double top, const double bot, const string txt, const color c)
  {
   ArrayResize(g_mb, g_nMb + 1);
   g_mb[g_nMb].b0 = b0; g_mb[g_nMb].b1 = b1; g_mb[g_nMb].top = top; g_mb[g_nMb].bot = bot; g_mb[g_nMb].txt = txt; g_mb[g_nMb].col = c;
   g_nMb++;
   while(g_nMb > 30) { for(int j = 0; j < g_nMb - 1; j++) g_mb[j] = g_mb[j + 1]; g_nMb--; }
  }

int NewFvg(const double top, const double bot, const int d, const double ext, const int i)
  {
   while(g_nFvg >= EFvgKeep) { for(int j = 0; j < g_nFvg - 1; j++) g_fvg[j] = g_fvg[j + 1]; g_nFvg--; }
   ArrayResize(g_fvg, g_nFvg + 1);
   g_fvg[g_nFvg].top = top; g_fvg[g_nFvg].bot = bot; g_fvg[g_nFvg].dir = d; g_fvg[g_nFvg].bar = i; g_fvg[g_nFvg].state = 0;
   g_fvg[g_nFvg].ext = ext; g_fvg[g_nFvg].uni = false; g_fvg[g_nFvg].bp = false; g_fvg[g_nFvg].nm = "FVG";
   g_nFvg++;
   return g_nFvg - 1;
  }

void FvgBpr(const int fi, const int dirWanted, const double minSz, const int i)
  {
   if(!EBpr) return;
   for(int k = 0; k < g_nFvg; k++)
     {
      if(k == fi) continue;
      if(g_fvg[k].dir == dirWanted && g_fvg[k].state == 0 && !g_fvg[k].bp && i - g_fvg[k].bar <= 40)
        {
         double oT = MathMin(g_fvg[fi].top, g_fvg[k].top), oB = MathMax(g_fvg[fi].bot, g_fvg[k].bot);
         if(oT - oB >= minSz)
           {
            g_fvg[fi].bp = true; g_fvg[k].bp = true;
            AddMisc(MathMin(g_fvg[k].bar, i) - 2, i + 6, oT, oB, "BPR", C_PURPLE);
           }
        }
     }
  }

void FvgDetect(const int i)
  {
   if(!InpFvgOn || i < 2 || g_atr[i] <= 0) return;
   double atr = g_atr[i], minSz = EFvgMin * atr;
   if(g_l[i] > g_h[i - 2] && (g_l[i] - g_h[i - 2]) >= minSz && (!EQual || (MidBull(i - 1) && g_c[i - 1] > g_h[i - 2])))
     {
      int fi = NewFvg(g_l[i], g_h[i - 2], 1, MathMin(g_l[i], MathMin(g_l[i - 1], g_l[i - 2])), i);
      g_evFvg = 1; g_nfTop = g_fvg[fi].top; g_nfBot = g_fvg[fi].bot; g_nfExt = g_fvg[fi].ext; g_nfDir = 1;
      FvgBpr(fi, -1, minSz, i);
     }
   if(g_h[i] < g_l[i - 2] && (g_l[i - 2] - g_h[i]) >= minSz && (!EQual || (MidBear(i - 1) && g_c[i - 1] < g_l[i - 2])))
     {
      int fi = NewFvg(g_l[i - 2], g_h[i], -1, MathMax(g_h[i], MathMax(g_h[i - 1], g_h[i - 2])), i);
      g_evFvg = -1; g_nfTop = g_fvg[fi].top; g_nfBot = g_fvg[fi].bot; g_nfExt = g_fvg[fi].ext; g_nfDir = -1;
      FvgBpr(fi, 1, minSz, i);
     }
   if(EVi)                                        // volume imbalance (bodies gap, wicks overlap)
     {
      double vs = 0.05 * atr;
      if(MathMin(g_o[i], g_c[i]) - MathMax(g_o[i - 1], g_c[i - 1]) >= vs && g_l[i] <= g_h[i - 1])
         AddMisc(i - 1, i + 8, MathMin(g_o[i], g_c[i]), MathMax(g_o[i - 1], g_c[i - 1]), "VI", C_BULL);
      if(MathMin(g_o[i - 1], g_c[i - 1]) - MathMax(g_o[i], g_c[i]) >= vs && g_h[i] >= g_l[i - 1])
         AddMisc(i - 1, i + 8, MathMin(g_o[i - 1], g_c[i - 1]), MathMax(g_o[i], g_c[i]), "VI", C_BEAR);
     }
  }

void FvgUpdate(const int i)
  {
   for(int k = g_nFvg - 1; k >= 0; k--)
     {
      bool kill = false;
      if(i > g_fvg[k].bar)
        {
         if(g_fvg[k].state == 0)
           {
            if(g_fvg[k].dir == 1)                 // bullish gap is filled from above
              {
               if(g_l[i] < g_fvg[k].bot)
                 {
                  if(g_c[i] < g_fvg[k].bot && EIfvg) { g_fvg[k].state = 1; g_fvg[k].dir = -1; g_fvg[k].nm = "IFVG"; }
                  else kill = true;
                 }
               else if(InpFvgShrink && g_l[i] < g_fvg[k].top) g_fvg[k].top = MathMax(g_l[i], g_fvg[k].bot);
              }
            else                                  // bearish gap is filled from below
              {
               if(g_h[i] > g_fvg[k].top)
                 {
                  if(g_c[i] > g_fvg[k].top && EIfvg) { g_fvg[k].state = 1; g_fvg[k].dir = 1; g_fvg[k].nm = "IFVG"; }
                  else kill = true;
                 }
               else if(InpFvgShrink && g_h[i] > g_fvg[k].bot) g_fvg[k].bot = MathMin(g_h[i], g_fvg[k].top);
              }
           }
         else
           {
            if(g_fvg[k].dir == -1 && g_c[i] > g_fvg[k].top) kill = true;       // inverted FVG dies when price closes back through
            if(g_fvg[k].dir == 1 && g_c[i] < g_fvg[k].bot) kill = true;
           }
        }
      if(kill)
        {
         for(int j = k; j < g_nFvg - 1; j++) g_fvg[j] = g_fvg[j + 1];
         g_nFvg--;
        }
     }
  }

//+------------------------------------------------------------------+
//| unicorn                                                          |
//+------------------------------------------------------------------+
void Unicorn(const int i)
  {
   if(!InpUniOn) return;
   for(int a = 0; a < g_nZn; a++)
     {
      if(!(g_zn[a].kind == 2 && !g_zn[a].uni && g_mssDir == g_zn[a].dir && g_zn[a].born >= g_mssBar && (g_t[i] - g_mssT) <= AgeSec())) continue;
      for(int b = 0; b < g_nFvg; b++)
        {
         if(g_zn[a].uni) break;
         if(!(!g_fvg[b].uni && g_fvg[b].state == 0 && g_fvg[b].dir == g_zn[a].dir && g_fvg[b].bar >= g_mssBar - 3 && g_fvg[b].bar <= g_mssBar + InpUniWin)) continue;
         double oT = MathMin(g_zn[a].top, g_fvg[b].top), oB = MathMax(g_zn[a].bot, g_fvg[b].bot);
         if(oT > oB)
           {
            g_zn[a].uni = true; g_fvg[b].uni = true;
            AddMisc(MathMin(g_zn[a].bar, g_fvg[b].bar), i + 10, oT, oB, "Unicorn", g_fvg[b].dir == 1 ? C_CYAN : C_ORANGE);
            g_evUni = g_fvg[b].dir; g_uniTop = oT; g_uniBot = oB; g_uniDir = g_fvg[b].dir; g_uniBar = i; g_uniDone = false;
            g_uniFar = g_fvg[b].dir == 1 ? g_zn[a].bot : g_zn[a].top;
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| structure: swings, dealing range, MSS / BOS                      |
//+------------------------------------------------------------------+
void ComputeDealing()
  {
   g_dealOk = false;
   if(IsNa(g_swHi) || IsNa(g_swLo) || !(g_swHi > g_swLo)) return;
   g_dealOk = true;
   double r = g_swHi - g_swLo;
   g_eq = g_swLo + r / 2.0; g_rngTop = g_swHi; g_rngBot = g_swLo;
   g_oteFired = false;
   if(g_swLoBar < g_swHiBar)
     {
      g_oteDir = 1; g_oteTop = g_swHi - InpOteLo * r; g_oteBot = g_swHi - InpOteHi * r; g_oteMid = g_swHi - 0.705 * r;
      g_oteSl = g_swLo; g_oteTp = g_swHi + 0.27 * r;
     }
   else
     {
      g_oteDir = -1; g_oteBot = g_swLo + InpOteLo * r; g_oteTop = g_swLo + InpOteHi * r; g_oteMid = g_swLo + 0.705 * r;
      g_oteSl = g_swHi; g_oteTp = g_swLo - 0.27 * r;
     }
  }

void PivotsM(const int i)
  {
   int len = InpSwLen, c = i - len;
   if(c - len < 0) return;
   bool ph = PivotHigh(c, len), pl = PivotLow(c, len), changed = false;
   double atr = g_atr[i];
   if(ph)
     {
      g_pSwHi = g_swHi; g_swHi = g_h[c]; g_swHiBar = c; g_swHiX = false; changed = true;
      if(InpEqOn && !IsNa(g_pSwHi) && atr > 0 && MathAbs(g_swHi - g_pSwHi) <= InpEqTol * atr)
         AddLiq(MathMax(g_swHi, g_pSwHi), "EQH", 1, 5, g_swHiBar, i);
      if(ERb)
        {
         double wk = g_h[c] - MathMax(g_o[c], g_c[c]), cr = g_h[c] - g_l[c];
         if(cr > 0 && wk >= InpRbPct * cr && wk >= InpRbAtr * atr) AddZone(g_h[c], MathMax(g_o[c], g_c[c]), -1, 4, c, "Bear Rejection", i);
        }
     }
   if(pl)
     {
      g_pSwLo = g_swLo; g_swLo = g_l[c]; g_swLoBar = c; g_swLoX = false; changed = true;
      if(InpEqOn && !IsNa(g_pSwLo) && atr > 0 && MathAbs(g_swLo - g_pSwLo) <= InpEqTol * atr)
         AddLiq(MathMin(g_swLo, g_pSwLo), "EQL", -1, 5, g_swLoBar, i);
      if(ERb)
        {
         double wk = MathMin(g_o[c], g_c[c]) - g_l[c], cr = g_h[c] - g_l[c];
         if(cr > 0 && wk >= InpRbPct * cr && wk >= InpRbAtr * atr) AddZone(MathMin(g_o[c], g_c[c]), g_l[c], 1, 4, c, "Bull Rejection", i);
        }
     }
   if(changed) ComputeDealing();
  }

void AddStruct(const double px, const int fromBar, const int toBar, const string txt, const color c)
  {
   ArrayResize(g_st, g_nSt + 1);
   g_st[g_nSt].px = px; g_st[g_nSt].fromBar = fromBar; g_st[g_nSt].toBar = toBar; g_st[g_nSt].txt = txt; g_st[g_nSt].col = c;
   g_nSt++;
   while(g_nSt > EStructKeep) { for(int j = 0; j < g_nSt - 1; j++) g_st[j] = g_st[j + 1]; g_nSt--; }
  }

void Breaks(const int i)
  {
   if(!IsNa(g_swHi) && !g_swHiX && g_c[i] > g_swHi)
     {
      g_swHiX = true; g_evBrk = 1;
      bool dOk = i - g_dispBullBar <= 3;
      bool isMss = g_trend == -1 && (dOk || !InpMssNeedD);
      string tx = isMss ? (dOk ? "MSS " + Bolt() : "MSS") : (g_trend == -1 ? "CHoCH" : "BOS");
      if((isMss && InpShowMss) || (!isMss && EShowBos)) AddStruct(g_swHi, g_swHiBar, i, tx, C_BULL);
      if(isMss)
        {
         g_mssDir = 1; g_mssBar = i; g_mssT = g_t[i]; g_evMss = 1;
         bool swept = !IsNa(g_pSwLo) && !IsNa(g_swLo) && g_swLo < g_pSwLo;
         if(swept) ZoneAt(g_swHiBar, false, 1, 2, "Bull Breaker", i);
         else ZoneAt(g_swLoBar, true, 1, 3, "Bull Mitigation", i);
        }
      else if(g_trend == 1 || g_trend == 0) ZoneAt(g_swLoBar, true, 1, 1, "Bull OB", i);
      g_trend = 1;
     }
   if(!IsNa(g_swLo) && !g_swLoX && g_c[i] < g_swLo)
     {
      g_swLoX = true; g_evBrk = -1;
      bool dOk = i - g_dispBearBar <= 3;
      bool isMss = g_trend == 1 && (dOk || !InpMssNeedD);
      string tx = isMss ? (dOk ? "MSS " + Bolt() : "MSS") : (g_trend == 1 ? "CHoCH" : "BOS");
      if((isMss && InpShowMss) || (!isMss && EShowBos)) AddStruct(g_swLo, g_swLoBar, i, tx, C_BEAR);
      if(isMss)
        {
         g_mssDir = -1; g_mssBar = i; g_mssT = g_t[i]; g_evMss = -1;
         bool swept = !IsNa(g_pSwHi) && !IsNa(g_swHi) && g_swHi > g_pSwHi;
         if(swept) ZoneAt(g_swLoBar, true, -1, 2, "Bear Breaker", i);
         else ZoneAt(g_swHiBar, false, -1, 3, "Bear Mitigation", i);
        }
      else if(g_trend == -1 || g_trend == 0) ZoneAt(g_swHiBar, false, -1, 1, "Bear OB", i);
      g_trend = -1;
     }
  }

//+------------------------------------------------------------------+
//| models and setups                                                |
//+------------------------------------------------------------------+
string ModelName(const int m) { return m == 1 ? "ICT 2022" : (m == 2 ? "Silver Bullet" : (m == 3 ? "Unicorn" : "OTE")); }

int Score(const int d, const bool kz, const int i)
  {
   int s = 0;
   if(kz) s++;
   if(g_raidDir == d && (g_t[i] - g_raidT) <= AgeSec()) s++;
   if(g_mssDir == d && (g_t[i] - g_mssT) <= AgeSec()) s++;
   if((d == 1 && i - g_dispBullBar <= 20) || (d == -1 && i - g_dispBearBar <= 20)) s++;
   if(g_htf[i] == d) s++;
   if(!IsNa(g_eq) && ((d == 1 && g_c[i] < g_eq) || (d == -1 && g_c[i] > g_eq))) s++;
   return s;
  }

double TargetPx(const int d, const double entry, const double minDist)
  {
   double best = TM_NA;
   for(int k = 0; k < g_nLq; k++)
     {
      if(g_lq[k].swept || g_lq[k].dir != d) continue;
      if(d == 1 && g_lq[k].price >= entry + minDist && (IsNa(best) || g_lq[k].price < best)) best = g_lq[k].price;
      if(d == -1 && g_lq[k].price <= entry - minDist && (IsNa(best) || g_lq[k].price > best)) best = g_lq[k].price;
     }
   return best;
  }

bool NewSetup(const int model, const int d, const double entry, const double sl, const bool inKz, const bool filled, const int i)
  {
   if(!InpSigOn) return false;
   double risk = MathAbs(entry - sl);
   bool dirOk = (d == 1) ? sl < entry : sl > entry;
   int sc = Score(d, inKz, i);
   bool htfOk = !InpHtfReq || g_htf[i] == d;
   if(!(risk > 0 && dirOk && htfOk && sc >= InpMinScore)) return false;
   double tp = TargetPx(d, entry, risk * InpMinRR);
   if(IsNa(tp)) tp = entry + d * risk * InpFbRR;
   double rr = MathAbs(tp - entry) / risk;
   if(rr < InpMinRR - 0.0001) return false;
   int oi = 0;                                    // only FINISHED setups may be dropped; open / pending trades never are
   while(g_nSu >= 24 && oi < g_nSu)
     {
      if(g_su[oi].state == 2) { for(int j = oi; j < g_nSu - 1; j++) g_su[j] = g_su[j + 1]; g_nSu--; }
      else oi++;
     }
   if(g_nSu >= 40) { g_capRej++; return false; }
   ArrayResize(g_su, g_nSu + 1);
   g_su[g_nSu].model = model; g_su[g_nSu].dir = d; g_su[g_nSu].entry = entry; g_su[g_nSu].sl = sl; g_su[g_nSu].tp = tp;
   g_su[g_nSu].born = i; g_su[g_nSu].fillBar = filled ? i : 0; g_su[g_nSu].state = filled ? 1 : 0; g_su[g_nSu].score = sc;
   g_su[g_nSu].rr = rr; g_su[g_nSu].cost = InpCostAtr * g_atr[i]; g_su[g_nSu].finBar = -1; g_su[g_nSu].win = false;
   g_nSu++;
   g_evSig = d;
   return true;
  }

void FinishSetup(const int k, const bool win, const int i)
  {
   g_su[k].state = 2; g_su[k].finBar = i; g_su[k].win = win;
   if(win) g_win[g_su[k].model]++; else g_loss[g_su[k].model]++;
  }

void UpdSetups(const int i)
  {
   for(int k = g_nSu - 1; k >= 0; k--)
     {
      if(g_su[k].state == 0)
        {
         bool cancel = i - g_su[k].born > InpExpBars;
         if(g_su[k].dir == 1 && (g_c[i] < g_su[k].sl || g_h[i] >= g_su[k].tp)) cancel = true;
         if(g_su[k].dir == -1 && (g_c[i] > g_su[k].sl || g_l[i] <= g_su[k].tp)) cancel = true;
         if(cancel)
           {
            for(int j = k; j < g_nSu - 1; j++) g_su[j] = g_su[j + 1];
            g_nSu--;
           }
         else if(i > g_su[k].born && ((g_su[k].dir == 1 && g_l[i] <= g_su[k].entry) || (g_su[k].dir == -1 && g_h[i] >= g_su[k].entry)))
           {
            g_su[k].state = 1; g_su[k].fillBar = i;
            bool slNow = (g_su[k].dir == 1) ? g_l[i] <= g_su[k].sl + g_su[k].cost : g_h[i] >= g_su[k].sl - g_su[k].cost;
            if(slNow) FinishSetup(k, false, i);
           }
        }
      else if(g_su[k].state == 1 && i > g_su[k].fillBar)
        {
         bool slHit = (g_su[k].dir == 1) ? g_l[i] <= g_su[k].sl + g_su[k].cost : g_h[i] >= g_su[k].sl - g_su[k].cost;     // pessimistic: the stop is checked first
         bool tpHit = (g_su[k].dir == 1) ? g_h[i] >= g_su[k].tp + g_su[k].cost : g_l[i] <= g_su[k].tp - g_su[k].cost;
         if(slHit) FinishSetup(k, false, i);
         else if(tpHit) FinishSetup(k, true, i);
        }
     }
  }

double EntryPx(const double top, const double bot, const int d)
  {
   if(InpEntryMode == MD_E_EDGE) return d == 1 ? top : bot;
   return (top + bot) / 2.0;
  }

void ModelsBar(const int i)
  {
   double buf = InpSlBuf * g_atr[i];
   bool kz = KzAny(i);
   if(InpM22On)                                   // ICT 2022: raid -> MSS -> FVG
     {
      if(g_evMss != 0 && g_raidDir == g_evMss && i - g_raidBar <= InpMssWin && (g_t[i] - g_raidT) <= AgeSec() && (!InpKzOnly || kz))
        { g_w22Dir = g_evMss; g_w22Bar = i; g_w22Px = g_raidPx; }
      if(g_w22Dir != 0)
        {
         if(i - g_w22Bar > 3) g_w22Dir = 0;
         else
           {
            bool done = false;
            for(int k = g_nFvg - 1; k >= 0 && !done; k--)
              {
               if(g_fvg[k].dir == g_w22Dir && g_fvg[k].state == 0 && g_fvg[k].bar >= g_w22Bar)
                 {
                  double en = EntryPx(g_fvg[k].top, g_fvg[k].bot, g_fvg[k].dir);
                  double sl = g_w22Dir == 1 ? g_w22Px - buf : g_w22Px + buf;
                  if(NewSetup(1, g_w22Dir, en, sl, kz, false, i)) { g_w22Dir = 0; done = true; }      // the chain ends only when a setup was really made
                 }
              }
           }
        }
     }
   if(!SbAny(i)) g_sbFired = false;               // Silver Bullet
   if(InpMSbOn && SbAny(i) && !g_sbFired && g_evFvg != 0)
     {
      int d = g_nfDir;
      bool aligned = (g_mssDir == d && (g_t[i] - g_mssT) <= AgeSec()) || (g_raidDir == d && (g_t[i] - g_raidT) <= AgeSec()) || !InpSbBias;
      if(aligned)
        {
         double en = EntryPx(g_nfTop, g_nfBot, d);
         double sl = d == 1 ? g_nfExt - buf : g_nfExt + buf;
         if(NewSetup(2, d, en, sl, true, false, i)) g_sbFired = true;
        }
     }
   if(InpMUniOn && g_uniDir != 0 && !g_uniDone && i - g_uniBar <= 3 && (!InpKzOnly || kz))      // Unicorn: retried for 3 bars, ONE setup at most
     {
      int d = g_uniDir;
      double en = EntryPx(g_uniTop, g_uniBot, d);
      double sl = d == 1 ? g_uniFar - buf : g_uniFar + buf;
      if(NewSetup(3, d, en, sl, kz, false, i)) g_uniDone = true;
     }
   if(InpMOteOn && g_oteDir != 0 && !g_oteFired && (!InpKzOnly || kz))   // OTE: a limit order at the signal candle's close
     {
      bool origin = (InpOteSL == MD_O_ORIGIN);
      if(g_oteDir == 1 && g_l[i] <= g_oteTop && g_c[i] > g_oteBot && g_c[i] > g_oteMid && g_c[i] > g_o[i])
        {
         double slv = origin ? g_oteSl : MathMin(g_l[i], g_oteBot);
         if(NewSetup(4, 1, g_c[i], slv - buf, kz, false, i)) g_oteFired = true;
        }
      if(g_oteDir == -1 && g_h[i] >= g_oteBot && g_c[i] < g_oteTop && g_c[i] < g_oteMid && g_c[i] < g_o[i])
        {
         double slv = origin ? g_oteSl : MathMax(g_h[i], g_oteTop);
         if(NewSetup(4, -1, g_c[i], slv + buf, kz, false, i)) g_oteFired = true;
        }
     }
   UpdSetups(i);
  }

//+------------------------------------------------------------------+
//| one confirmed bar (same order as the Pine main block)            |
//+------------------------------------------------------------------+
void ProcessBar(const int i)
  {
   g_evRaid = 0; g_evRun = 0; g_evMss = 0; g_evBrk = 0; g_evFvg = 0; g_evUni = 0; g_evSig = 0;
   if(DispBull(i)) { g_dispBullBar = i; if(EDispMark) AddDisp(i, 1); }
   if(DispBear(i)) { g_dispBearBar = i; if(EDispMark) AddDisp(i, -1); }
   if(g_intra)
      for(int k = 0; k < 5; k++)
        {
         bool ended = SessUpd(k, Fl(i, k), i);
         if(ended) SessEnd(k, i);
        }
   PeriodsBar(i);
   FvgDetect(i);
   FvgUpdate(i);
   PivotsM(i);
   ChkLiq(i);
   Breaks(i);
   UpdZones(i);
   Gaps(i);
   Unicorn(i);
   ModelsBar(i);
  }

//+------------------------------------------------------------------+
//| drawing                                                          |
//+------------------------------------------------------------------+
color ZoneCol(const int kind, const int d)
  {
   if(kind == 2) return d == 1 ? C_CYAN : C_ORANGE;
   if(kind == 3) return d == 1 ? C_PURPLE : C_PINK;
   if(kind == 4) return C_YELLOW;
   if(kind == 5) return C_LIME;
   if(kind == 6) return C_GRAY;
   return d == 1 ? InpObBullC : InpObBearC;
  }

color LiqColor(const int kind, const string nm)
  {
   if(kind == 2) return InpPdC;
   if(kind == 3) return InpPwC;
   if(kind == 4) return InpPmC;
   if(kind == 5) return InpEqC;
   return InpSessLC;
  }

double g_near = 0.0;

void DrawFvgs()
  {
   datetime xe = TimeAt(g_n - 1 + InpLblPad - 1);
   for(int k = 0; k < g_nFvg; k++)
     {
      color c = g_fvg[k].dir == 1 ? (g_fvg[k].state == 1 ? C_CYAN : InpFvgBullC) : (g_fvg[k].state == 1 ? C_ORANGE : InpFvgBearC);
      datetime t1 = g_t[MathMax(0, g_fvg[k].bar - 2)];
      MkRect("FV", t1, g_fvg[k].top, xe, g_fvg[k].bot, Tint(c, 0.14), true);
      MkRect("FB", t1, g_fvg[k].top, xe, g_fvg[k].bot, Tint(c, 0.6), false);
      double mid = (g_fvg[k].top + g_fvg[k].bot) / 2.0;
      if(InpFvgCE) MkLine("FC", t1, mid, xe, mid, Tint(c, 0.7), STYLE_DOT, 1);
      if(MathAbs(mid - g_c[g_n - 1]) <= g_near) AddItem(g_fvg[k].nm, c, mid);
     }
  }

void DrawZones()
  {
   datetime xe = TimeAt(g_n - 1 + InpLblPad - 1);
   for(int k = 0; k < g_nZn; k++)
     {
      color c = ZoneCol(g_zn[k].kind, g_zn[k].dir);
      datetime t1 = g_t[MathMax(0, g_zn[k].bar)];
      MkRect("ZF", t1, g_zn[k].top, xe, g_zn[k].bot, Tint(c, g_zn[k].touched ? 0.08 : 0.15), true);
      MkRect("ZB", t1, g_zn[k].top, xe, g_zn[k].bot, Tint(c, 0.7), false);
      double mid = (g_zn[k].top + g_zn[k].bot) / 2.0;
      if(EZoneMt) MkLine("ZM", t1, mid, xe, mid, Tint(c, 0.6), STYLE_DOT, 1);
      if(MathAbs(mid - g_c[g_n - 1]) <= g_near) AddItem(g_zn[k].nm, c, mid);
     }
  }

void DrawMisc()
  {
   for(int k = 0; k < g_nMb; k++)
     {
      int b0 = MathMax(0, g_mb[k].b0);
      datetime t1 = g_t[b0], t2 = TimeAt(g_mb[k].b1);
      bool uni = (g_mb[k].txt == "Unicorn");
      MkRect("MB", t1, g_mb[k].top, t2, g_mb[k].bot, Tint(g_mb[k].col, uni ? 0.30 : 0.22), true);
      MkRect("MR", t1, g_mb[k].top, t2, g_mb[k].bot, uni ? clrWhite : Tint(g_mb[k].col, 0.9), false);
      MkText("MT", t1 + (datetime)((t2 - t1) / 2), (g_mb[k].top + g_mb[k].bot) / 2.0, g_mb[k].txt, uni ? clrWhite : g_mb[k].col, ANCHOR_CENTER);
     }
  }

void DrawStructure()
  {
   for(int k = 0; k < g_nSt; k++)
     {
      int f = Cl1400(g_st[k].fromBar);
      MkLine("SL", g_t[f], g_st[k].px, g_t[g_st[k].toBar], g_st[k].px, g_st[k].col, STYLE_DASH, 1);
      MkText("ST", g_t[(f + g_st[k].toBar) / 2], g_st[k].px, g_st[k].txt, g_st[k].col, ANCHOR_LOWER);
     }
  }

void DrawLiquidity()
  {
   datetime xe = TimeAt(g_n - 1 + InpLblPad - 1);
   for(int k = 0; k < g_nLq; k++)
     {
      color c = LiqColor(g_lq[k].kind, g_lq[k].name);
      datetime t0 = g_t[MathMax(g_lq[k].ob, MathMax(0, g_n - 1400))];
      if(g_lq[k].swept)
        {
         MkLine("LQ", t0, g_lq[k].price, g_t[g_lq[k].sweptBar], g_lq[k].price, c, STYLE_DOT, 1);
         MkText("LC", g_t[g_lq[k].sweptBar], g_lq[k].price, Ck(), c, ANCHOR_LEFT);
        }
      else
        {
         MkLine("LQ", t0, g_lq[k].price, xe, g_lq[k].price, c, STYLE_DASH, 1);
         if(MathAbs(g_lq[k].price - g_c[g_n - 1]) <= g_near * 1.5) AddItem(g_lq[k].name + " " + Fp(g_lq[k].price), c, g_lq[k].price);
        }
     }
  }

void DrawDealing()
  {
   if(!g_dealOk) return;
   datetime xe = TimeAt(g_n - 1 + InpLblPad - 1);
   datetime x0 = g_t[Cl1400(MathMin(g_swHiBar, g_swLoBar))];
   if(EPdZone)
     {
      MkRect("PD", x0, g_swHi, xe, g_eq, Tint(C_BEAR, 0.06), true);
      MkRect("PD", x0, g_eq, xe, g_swLo, Tint(C_BULL, 0.06), true);
      MkLine("PE", x0, g_eq, xe, g_eq, Tint(C_GRAY, 0.8), STYLE_DASH, 1);
      AddItem("Premium", C_BEAR, g_swHi);
      AddItem("EQ " + Fp(g_eq), C_GRAY, g_eq);
      AddItem("Discount", C_BULL, g_swLo);
     }
   if(InpOteOn)
     {
      color oc = g_oteDir == 1 ? C_BULL : C_BEAR;
      datetime xo = g_t[Cl1400(MathMax(g_swHiBar, g_swLoBar))];
      MkRect("OF", xo, g_oteTop, xe, g_oteBot, Tint(oc, 0.18), true);
      MkRect("OB", xo, g_oteTop, xe, g_oteBot, Tint(oc, 0.6), false);
      MkLine("OM", xo, g_oteMid, xe, g_oteMid, Tint(oc, 0.8), STYLE_DOT, 1);
      AddItem("OTE 70.5%", oc, g_oteMid);
     }
  }

void DrawMarks()
  {
   for(int k = 0; k < g_nMk; k++)
     {
      if(!g_mk[k].alive) continue;
      int cnt = 0;                                // several markers on the same bar are stacked
      for(int j = 0; j < k; j++) if(g_mk[j].alive && g_mk[j].bar == g_mk[k].bar) cnt++;
      double off = cnt * 0.7 * g_atr[MathMin(g_mk[k].bar, g_n - 1)];
      double y = g_mk[k].dir == 1 ? g_mk[k].price + off : g_mk[k].price - off;
      MkText("MK", g_t[g_mk[k].bar], y, g_mk[k].text, g_mk[k].col, g_mk[k].dir == 1 ? ANCHOR_LOWER : ANCHOR_UPPER);
     }
   for(int k = 0; k < g_nDp; k++)
      MkText("DP", g_t[g_dp[k].bar], (g_o[g_dp[k].bar] + g_c[g_dp[k].bar]) / 2.0, Bolt(), g_dp[k].dir == 1 ? C_BULL : C_BEAR, ANCHOR_CENTER);
  }

void DrawSetups()
  {
   if(!InpSigOn || !InpShowSetups) return;
   datetime xe = TimeAt(g_n - 1 + InpLblPad - 1);
   for(int k = 0; k < g_nSu; k++)
     {
      datetime t0 = g_t[g_su[k].born];
      datetime t1 = (g_su[k].state == 2) ? g_t[g_su[k].finBar] : xe;
      bool fin = (g_su[k].state == 2);
      MkLine("SE", t0, g_su[k].entry, t1, g_su[k].entry, fin ? Tint(C_GRAY, 0.8) : C_BLUE, STYLE_SOLID, 2);
      MkLine("SS", t0, g_su[k].sl, t1, g_su[k].sl, (fin && !g_su[k].win) ? Tint(C_GRAY, 0.3) : C_BEAR, STYLE_SOLID, 1);
      MkLine("SU", t0, g_su[k].tp, t1, g_su[k].tp, (fin && g_su[k].win) ? Tint(C_GRAY, 0.3) : C_BULL, STYLE_SOLID, 1);
      string arrow = g_su[k].dir == 1 ? " " + Up() + " " : " " + Dn() + " ";
      if(fin)
         MkText("SF", t1, g_su[k].entry, ModelName(g_su[k].model) + arrow + (g_su[k].win ? "TP " + Ck() : "SL x"), g_su[k].win ? C_BULL : C_BEAR, ANCHOR_LEFT);
      else
        {
         string lb = ModelName(g_su[k].model) + (g_su[k].dir == 1 ? " BUY " : " SELL ") + "*" + IntegerToString(g_su[k].score) + "/6  RR " + DoubleToString(g_su[k].rr, 1);
         if(g_su[k].state == 1) lb = ModelName(g_su[k].model) + arrow + "filled *" + IntegerToString(g_su[k].score);
         AddItem(lb, g_su[k].dir == 1 ? C_BULL : C_BEAR, g_su[k].entry);
        }
     }
  }

//+------------------------------------------------------------------+
//| panel                                                            |
//+------------------------------------------------------------------+
string DirTxt(const int d) { return d == 1 ? "Bullish " + Up() : (d == -1 ? "Bearish " + Dn() : "-"); }
color  DirCol(const int d) { return d == 1 ? C_BULL : (d == -1 ? C_BEAR : C_GRAY); }

string TfName(const ENUM_TIMEFRAMES tf)
  {
   int m = PeriodSeconds(tf) / 60;
   if(m < 60) return "M" + IntegerToString(m);
   if(m < 1440) return "H" + IntegerToString(m / 60);
   return "D" + IntegerToString(m / 1440);
  }

void UpdatePanel()
  {
   if(!InpShowPanel)
     {
      ObjectsDeleteAll(0, PFX + "PN");
      return;
     }
   if(g_n < 3) return;
   ChooseCorner();
   int L = g_n - 2;
   string actNm = "Outside killzones";
   color  actCol = C_GRAY;
   for(int k = 0; k < 5; k++) if(g_ms[k].on && g_ms[k].active) { actNm = g_ms[k].name; actCol = g_ms[k].col; }
   if(SbAny(L)) actNm += " + Silver Bullet";
   string rd = g_raidDir == 0 ? "-" : g_raidName + (g_raidRej ? " (raid)" : " (run)") + " - " + IntegerToString(L - g_raidBar) + " bars ago";
   string ms = g_mssDir == 0 ? "-" : DirTxt(g_mssDir) + " - " + IntegerToString(L - g_mssBar) + " bars ago";
   string pdTxt = "-";
   color  pdCol = C_GRAY;
   if(!IsNa(g_rngTop) && g_rngTop > g_rngBot)
     {
      double pos = (g_c[L] - g_rngBot) / (g_rngTop - g_rngBot) * 100.0;
      if(pos > 100.0) { pdTxt = "Above range (new highs)"; pdCol = C_BEAR; }
      else if(pos < 0.0) { pdTxt = "Below range (new lows)"; pdCol = C_BULL; }
      else { pdTxt = (pos >= 50.0 ? "Premium " : "Discount ") + IntegerToString((int)pos) + "%"; pdCol = pos >= 50.0 ? C_BEAR : C_BULL; }
     }
   string mode = "Analysis only (no signals)";
   color  modeCol = C_GRAY;
   if(InpSigOn)
     {
      int nOpen = 0;
      for(int k = 0; k < g_nSu; k++) if(g_su[k].state < 2) nOpen++;
      mode = "Setups: " + IntegerToString(nOpen) + " open";
      if(EStats)
        {
         mode = "";
         for(int m = 1; m <= 4; m++) mode += ModelName(m) + " " + IntegerToString(g_win[m]) + "W/" + IntegerToString(g_loss[m]) + "L  ";
        }
      if(g_capRej > 0) mode += " (rejected " + IntegerToString(g_capRej) + ": 40 open)";
      modeCol = C_ORANGE;
     }
   string bk = "GMT";
   if(InpBrokerGmt >= 0) bk += "+";
   bk += IntegerToString(InpBrokerGmt);
   if(InpBrokerDst != TM_DST_NONE) bk += " (+1 summer)";
   color  bkC = C_GRAY;
#ifdef __MQL5__
   datetime srv = TimeTradeServer(), gm = TimeGMT();
   if(srv > 0 && gm > 0)
     {
      int real = (int)MathRound((double)(srv - gm) / 1800.0);
      int mine = (int)MathRound((double)(srv - ServerToUtc(srv)) / 1800.0);
      if(real != mine) { bk += "  CHECK offset!"; bkC = C_BEAR; } else bkC = C_BULL;
     }
#endif
   PanelRow(0, "Crypex ICT Models", g_intra ? "" : "use <= H4", C_ORANGE);
   PanelRow(1, "Now", actNm, actCol);
   PanelRow(2, "HTF bias " + TfName(InpBiasTF) + "/" + IntegerToString(InpBiasLen), DirTxt(g_htf[L]), DirCol(g_htf[L]));
   PanelRow(3, "Last liquidity", rd, DirCol(g_raidDir));
   PanelRow(4, "Last MSS", ms, DirCol(g_mssDir));
   PanelRow(5, "Dealing range", pdTxt, pdCol);
   PanelRow(6, "Mode", mode, modeCol);
   PanelRow(7, "Broker clock", bk, bkC);
   PanelMeasure();
   int px = 0, py = 0;
   PanelOrigin(px, py);
   PanelBack(px, py);
   for(int r = 0; r < PN_ROWS; r++) PanelDrawRow(r, px, py);
  }

//+------------------------------------------------------------------+
//| alerts                                                           |
//+------------------------------------------------------------------+
void FireAlerts()
  {
   string names[10] = {"Buy-side liquidity raided (bearish expectation)", "Sell-side liquidity raided (bullish expectation)",
                       "Liquidity run (level broken and held, not a raid)", "Bullish MSS", "Bearish MSS", "New bullish FVG", "New bearish FVG",
                       "Unicorn zone formed", "BUY setup", "SELL setup"};
   for(int k = 0; k < 10; k++) if(g_alLast[k]) Notify(names[k]);
  }

void FlagAlerts()
  {
   for(int k = 0; k < 10; k++) g_alLast[k] = false;
   g_alLast[0] = (g_evRaid == -1); g_alLast[1] = (g_evRaid == 1); g_alLast[2] = (g_evRun != 0);
   g_alLast[3] = (g_evMss == 1);   g_alLast[4] = (g_evMss == -1);
   g_alLast[5] = (g_evFvg == 1);   g_alLast[6] = (g_evFvg == -1);
   g_alLast[7] = (g_evUni != 0);
   g_alLast[8] = (g_evSig == 1);   g_alLast[9] = (g_evSig == -1);
  }

//+------------------------------------------------------------------+
//| replay the whole window bar by bar, then draw the final state    |
//+------------------------------------------------------------------+
void Rebuild()
  {
   ObjectsDeleteAll(0, PFX);
   g_bg = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   g_intra = (PeriodSeconds(_Period) <= 14400);
   DetectMarket();
   InitModels();
   ResetState();
   BuildSeries();
   const int L = g_n - 2;                          // last CLOSED bar: no repainting
   for(int i = 0; i <= L; i++) ProcessBar(i);
   FlagAlerts();
   g_nItem = 0;
   g_near = Rng150() * ELblNear;
   DrawDealing();
   DrawFvgs();
   DrawZones();
   DrawMisc();
   DrawStructure();
   DrawLiquidity();
   DrawSetups();
   FlushItems();
   DrawMarks();
   UpdatePanel();
   PaintWatermark();
   if(g_firstDone && g_t[g_n - 1] != g_lastAlertBar) FireAlerts();
   g_lastAlertBar = g_t[g_n - 1];
   g_firstDone = true;
   ChartRedraw();
  }

//+------------------------------------------------------------------+
int OnInit()
  {
   IndicatorSetString(INDICATOR_SHORTNAME, "Crypex ICT Models");
   if(InpRightMargin > 0)
     {
      ChartSetInteger(0, CHART_SHIFT, true);
      ChartSetDouble(0, CHART_SHIFT_SIZE, (double)MathMin(InpRightMargin, 50));
     }
   return INIT_SUCCEEDED;
  }

void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, PFX);
  }

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_CHART_CHANGE && g_n >= 3) UpdatePanel();      // scroll / zoom / resize: re-pick the panel corner
  }

int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[],
                const double &open[], const double &high[], const double &low[], const double &close[],
                const long &tick_volume[], const long &volume[], const int &spread[])
  {
   if(rates_total < 300) return 0;
   bool ser = ArrayGetAsSeries(time);               // normally false (oldest first); handled either way
   datetime newest = ser ? time[0] : time[rates_total - 1];
   bool newBar = (newest != g_lastBar) || prev_calculated == 0;
   if(!newBar)
     {
      UpdatePanel();                               // the clock runs on every tick
      return rates_total;
     }
   g_lastBar = newest;
   int start = MathMax(0, rates_total - InpMaxBars);
   g_n = rates_total - start;
   ArrayResize(g_t, g_n); ArrayResize(g_o, g_n); ArrayResize(g_h, g_n); ArrayResize(g_l, g_n); ArrayResize(g_c, g_n);
   for(int i = 0; i < g_n; i++)
     {
      int s = ser ? rates_total - 1 - (start + i) : start + i;
      g_t[i] = time[s];
      g_o[i] = open[s];
      g_h[i] = high[s];
      g_l[i] = low[s];
      g_c[i] = close[s];
     }
   Rebuild();
   return rates_total;
  }
//+------------------------------------------------------------------+
ict-models-crypex.mq4 · MQL4 · ۲٬۰۲۴ خط
//+------------------------------------------------------------------+
//| Crypex ICT Models (MT4)                                        |
//| Market structure (MSS / BOS), Fair Value Gaps (CE, IFVG, VI,     |
//| BPR), Order Block, Breaker, Mitigation, Unicorn, Premium /       |
//| Discount, OTE, Raid / Run, panel. Analysis only by default;      |
//| ICT 2022 / Silver Bullet / Unicorn / OTE setups are optional     |
//| © Crypex.org — All rights reserved. | حق نشر محفوظ برای Crypex.org |
//| License: personal, non-commercial use only. Redistribution or    |
//| resale without written permission is prohibited.                 |
//| مجوز: استفاده شخصی و غیرتجاری. بازنشر یا فروش مجدد بدون          |
//| اجازه‌ی کتبی ممنوع است.                                           |
//| Educational tool, not investment advice.                         |
//+------------------------------------------------------------------+
#property copyright "Crypex.org"
#property link      "https://crypex.org"
#property version   "1.00"
#property strict
#property indicator_chart_window

enum ENUM_TM_DST    { TM_DST_US, TM_DST_EU, TM_DST_NONE };
enum ENUM_TM_ZONE   { TM_Z_TEHRAN, TM_Z_NY, TM_Z_UTC, TM_Z_LONDON };
enum ENUM_TM_MARKET { TM_M_AUTO, TM_M_FOREX, TM_M_METALS, TM_M_CRYPTO };
enum ENUM_TM_CORNER { TM_C_AUTO, TM_C_TOPRIGHT, TM_C_TOPLEFT, TM_C_BOTTOMRIGHT, TM_C_BOTTOMLEFT };
enum ENUM_MD_PRESET { MD_P_CLEAN, MD_P_STANDARD, MD_P_FULL, MD_P_CUSTOM };
enum ENUM_MD_ZMODE  { MD_Z_FULL, MD_Z_BODY, MD_Z_OPEN };
enum ENUM_MD_ENTRY  { MD_E_CE, MD_E_EDGE };
enum ENUM_MD_OTESL  { MD_O_BEYOND, MD_O_ORIGIN };

// ---- General & time zones
input ENUM_MD_PRESET   InpPreset       = MD_P_CLEAN;      // Preset (Clean = only the most valuable items, Standard, Full, Custom = my own settings below)
input int              InpBrokerGmt    = 2;               // Broker server time: GMT offset in WINTER (e.g. 2 for GMT+2 / GMT+3 brokers)
input ENUM_TM_DST      InpBrokerDst    = TM_DST_US;       // Broker daylight-saving rule (US = GMT+2/+3 New-York-close brokers)
input ENUM_TM_ZONE     InpTzDisp       = TM_Z_TEHRAN;     // Show times on chart in
input ENUM_TM_MARKET   InpMarket       = TM_M_AUTO;       // Market type (Auto = detect from the symbol name)
input int              InpFont         = 9;               // Label font size
input double           InpLblGap       = 3.0;             // Right-side labels: min vertical gap (% of recent range)
input int              InpLblPad       = 3;               // Right-side labels: offset from last bar (bars)
input double           InpLblNear      = 0.30;            // Show zone labels within (x recent range from price)
input int              InpRightMargin  = 15;              // Chart right margin (% of the chart, 0 = leave the chart alone)
// ---- Killzones (used by the models, New York time)
input bool             InpAsiaOn       = true;            // Asian range
input string           InpAsiaR        = "2000-0000";     //   Asian range
input bool             InpLonOn        = true;            // London killzone
input string           InpLonR         = "0200-0500";     //   London
input bool             InpNyaOn        = true;            // New York AM killzone
input string           InpNyaR         = "0700-1000";     //   NY AM
input bool             InpLcOn         = true;            // London Close killzone
input string           InpLcR          = "1000-1200";     //   London Close
input bool             InpNypOn        = true;            // New York PM killzone
input string           InpNypR         = "1330-1600";     //   NY PM
input bool             InpSessLvlOn    = true;            // Turn finished session highs/lows into liquidity levels (Custom preset)
input bool             InpSbOn         = true;            // Silver Bullet windows
input string           InpSb1R         = "0300-0400";     //   SB 1 (London)
input string           InpSb2R         = "1000-1100";     //   SB 2 (NY AM)
input string           InpSb3R         = "1400-1500";     //   SB 3 (NY PM)
// ---- Liquidity levels (raids & targets)
input bool             InpPdOn         = true;            // PDH / PDL
input color            InpPdC          = C'41,98,255';
input bool             InpPwOn         = true;            // PWH / PWL
input color            InpPwC          = C'156,39,176';
input bool             InpPmOn         = false;           // PMH / PML (Custom preset)
input color            InpPmC          = C'233,30,99';
input color            InpSessLC       = C'120,123,134';
input bool             InpEqOn         = true;            // EQH / EQL (equal highs / lows)
input color            InpEqC          = C'255,109,0';
input double           InpEqTol        = 0.10;            // EQ tolerance (x ATR)
input bool             InpRaidLbl      = true;            // Mark raids and runs
input int              InpPendBars     = 3;               // Bars to wait before a break counts as a Run
input int              InpKeepSwept    = 60;              // Keep swept levels for (bars) (Custom preset)
input int              InpMaxLiq       = 40;              // Max liquidity levels on chart (Custom preset)
// ---- Market structure & displacement
input int              InpSwLen        = 5;               // Swing length
input double           InpDispAtr      = 1.2;             // Displacement: body >= x ATR
input double           InpDispBody     = 0.6;             // Displacement: body / range >=
input bool             InpDispMark     = true;            // Mark displacement candles (Custom preset)
input bool             InpMssNeedD     = false;           // MSS needs displacement (else a plain break = BOS / CHoCH)
input bool             InpShowBos      = true;            // Show BOS (Custom preset)
input bool             InpShowMss      = true;            // Show MSS
input int              InpStructKeep   = 14;              // Structure lines to keep (Custom preset)
// ---- Premium / Discount & OTE
input bool             InpPdZoneOn     = true;            // Premium / Discount / Equilibrium zone (Custom preset)
input bool             InpOteOn        = true;            // OTE zone
input double           InpOteLo        = 0.62;            // OTE from
input double           InpOteHi        = 0.79;            // OTE to
// ---- Fair Value Gaps
input bool             InpFvgOn        = true;            // Fair Value Gaps
input double           InpFvgMin       = 0.10;            // Min FVG size (x ATR) (Custom preset)
input int              InpFvgKeep      = 16;              // Max active FVGs (Custom preset)
input bool             InpFvgQual      = false;           // Quality filter: middle candle must be a strong move (Custom preset)
input double           InpFvgMid       = 0.5;             // Quality filter: middle candle body >= x ATR
input bool             InpFvgCE        = true;            // Show Consequent Encroachment (50 %)
input bool             InpFvgShrink    = true;            // Shrink FVG when partly filled
input bool             InpIfvgOn       = true;            // Inversion FVG (IFVG) (Custom preset)
input bool             InpViOn         = true;            // Volume Imbalance (VI) (Custom preset)
input bool             InpBprOn        = true;            // Balanced Price Range (BPR) (Custom preset)
input color            InpFvgBullC     = C'8,153,129';
input color            InpFvgBearC     = C'242,54,69';
// ---- Blocks, Breaker, Mitigation, Rejection, Propulsion, Gaps, Unicorn
input bool             InpObOn         = true;            // Order Block (BOS continuation)
input bool             InpBrkOn        = true;            // Breaker Block (approx.)
input bool             InpMitOn        = true;            // Mitigation Block (approx.) (Custom preset)
input bool             InpRbOn         = true;            // Rejection Block (Custom preset)
input bool             InpPbOn         = true;            // Propulsion Block (Custom preset)
input bool             InpVacOn        = true;            // Vacuum Block / NWOG / NDOG (Custom preset)
input bool             InpUniOn        = true;            // Unicorn (Breaker + FVG overlap)
input ENUM_MD_ZMODE    InpZoneMode     = MD_Z_FULL;       // Block zone from
input bool             InpZoneMt       = true;            // Show 50 % mean threshold of blocks (Custom preset)
input int              InpZoneMax      = 24;              // Max blocks on chart (Custom preset)
input double           InpRbPct        = 0.5;             // Rejection: wick >= % of candle
input double           InpRbAtr        = 0.5;             // Rejection: wick >= x ATR
input double           InpGapAtr       = 0.3;             // Gap: min size (x ATR)
input color            InpObBullC      = C'8,153,129';
input color            InpObBearC      = C'242,54,69';
// ---- Models & signals
input bool             InpSigOn        = false;           // Trade setups: Entry / SL / TP signals (OFF = analysis only)
input bool             InpM22On        = true;            // Model: ICT 2022 (raid -> MSS -> FVG)
input bool             InpMSbOn        = true;            // Model: Silver Bullet (FVG inside window)
input bool             InpMUniOn       = true;            // Model: Unicorn
input bool             InpMOteOn       = true;            // Model: OTE
input int              InpUniWin       = 6;               // Unicorn: FVG must form within N bars after the MSS
input bool             InpKzOnly       = true;            // Signals only inside killzones / Silver Bullet
input int              InpMssWin       = 30;              // ICT 2022: max bars from raid to MSS
input ENUM_MD_ENTRY    InpEntryMode    = MD_E_CE;         // Entry on FVG
input double           InpSlBuf        = 0.10;            // Stop buffer (x ATR)
input double           InpMinRR        = 1.5;             // Min reward : risk
input double           InpFbRR         = 2.0;             // Fallback target (R) if no liquidity target
input int              InpExpBars      = 40;              // Cancel unfilled setup after (bars)
input int              InpMinScore     = 3;               // Min confluence score (0-6)
input bool             InpSbBias       = true;            // Silver Bullet needs raid or MSS in the same direction
input ENUM_TIMEFRAMES  InpBiasTF       = PERIOD_H4;       // HTF bias timeframe
input int              InpBiasLen      = 21;              // HTF bias EMA length
input ENUM_MD_OTESL    InpOteSL        = MD_O_BEYOND;     // OTE model stop
input int              InpAgeMin       = 240;             // Event validity for the score (minutes)
input bool             InpHtfReq       = false;           // Require HTF bias in the trade direction
input double           InpCostAtr      = 0.03;            // Trading cost (x ATR) in the win-rate stats
input bool             InpShowSetups   = true;            // Draw entry / SL / TP
// ---- Panel & watermark
input bool             InpShowPanel    = true;            // Show info panel
input ENUM_TM_CORNER   InpPanelPos     = TM_C_AUTO;       // Panel position (Auto = the emptiest corner of the visible chart)
input bool             InpShowStats    = false;           // Win / loss stats of the models in the panel (Custom preset, needs trade setups)
input bool             InpShowWm       = true;            // Show crypex.org watermark
input int              InpWmSize       = 18;
input int              InpWmGap        = 60;              // Gap from left (px), clears the chart logo
input int              InpWmBottom     = 14;              // Gap from bottom (px)
// ---- Alerts
input bool             InpAlertPopup   = true;
input bool             InpAlertPush    = false;
input bool             InpAlertMail    = false;
// ---- Calculation
input int              InpMaxBars      = 6000;            // Bars used for calculation

const string PFX = "CRX_MODELS_";
const double TM_NA = DBL_MAX;
const color  C_BULL = C'8,153,129', C_BEAR = C'242,54,69', C_BLUE = C'41,98,255', C_ORANGE = C'255,152,0', C_PURPLE = C'156,39,176';
const color  C_YELLOW = C'255,214,0', C_GRAY = C'120,123,134', C_CYAN = C'0,188,212', C_PINK = C'233,30,99';
const color  BRAND_GREEN = C'11,163,28', BRAND_DARK = C'30,34,41';

struct TmSess { string name; int a; int b; bool on; color col; bool proj; bool lvl; bool kz; bool active; int sBar; datetime sTime;
                double hi; double lo; int hiBar; int loBar; double fHi; double fLo; int fHiBar; int fLoBar; int boxId; };
struct TmBox  { int id; int s; int i0; int i1; double hi; double lo; datetime sTime; };
struct TmLiq  { double price; string name; int dir; int kind; int bar; int ob; bool swept; int sweptBar; bool pend; int hitBar; double hitExt; int mk; };
struct TmMark { int id; int bar; double price; string text; color col; int dir; int kind; bool alive; };
struct TmProj { bool ok; int i0; double hi; double lo; };
struct TmAnc  { string name; bool on; color col; bool has; int bar; double px; bool exact; };
struct TmAncH { int x1; int x2; double px; color col; };
struct TmItem { string text; color col; double price; };

// ---- chart copy (oldest first) and per-bar derived series
datetime g_t[];
double   g_o[], g_h[], g_l[], g_c[];
double   g_atr[], g_x2h[], g_x2l[];
datetime g_ny[];                       // New York wall clock of every bar (as a plain datetime)
int      g_mn[], g_dow[], g_day[], g_mon[], g_fl[];
int      g_n = 0;

TmSess   g_s[8];
TmBox    g_box[];   int g_nBox = 0;   int g_boxId = 0;
TmLiq    g_lq[];    int g_nLq = 0;
TmMark   g_mk[];    int g_nMk = 0;    int g_mkId = 0;
TmProj   g_pj[8];
TmAnc    g_an[4];
TmAncH   g_ah[];    int g_nAh = 0;

bool     g_intra = true, g_isCrypto = false, g_isForex = false;
// state (St object of the Pine version)
double   g_swHi = TM_NA, g_pSwHi = TM_NA, g_swLo = TM_NA, g_pSwLo = TM_NA;
int      g_swHiBar = 0, g_swLoBar = 0;
int      g_raidDir = 0, g_raidBar = -9999, g_raidSc = 0, g_raidScBar = -1, g_raidMk = -1;
double   g_raidPx = TM_NA, g_raidPen = 0.0;
string   g_raidName = "";
bool     g_raidRej = false;
double   g_curDH = TM_NA, g_curDL = TM_NA, g_curWH = TM_NA, g_curWL = TM_NA, g_curMH = TM_NA, g_curML = TM_NA;
int      g_curDHB = 0, g_curDLB = 0, g_curWHB = 0, g_curWLB = 0, g_curMHB = 0, g_curMLB = 0;
bool     g_jdDone = false;
int      g_jdPot = 0;
double   g_jdExt = TM_NA, g_moPx = TM_NA;
int      g_po3DnBar = -9999, g_po3UpBar = -9999, g_po3Bias = 0;
double   g_po3DnExt = TM_NA, g_po3UpExt = TM_NA, g_po3Ext = TM_NA;
double   g_phM = TM_NA, g_phS = TM_NA, g_plM = TM_NA, g_plS = TM_NA;
int      g_smtDir = 0, g_smtBar = -9999;
int      g_evRaid = 0, g_evRun = 0, g_evJudas = 0, g_evSmt = 0;
bool     g_al[10];
bool     g_alLast[10];
color    g_bg = clrBlack;
datetime g_lastBar = 0, g_lastAlertBar = 0;
bool     g_firstDone = false;
int      g_cnt = 0;
string   g_macroR[9] = {"0233-0300", "0403-0430", "0850-0910", "0950-1010", "1050-1110", "1150-1210", "1310-1340", "1450-1510", "1515-1545"};
int      g_macroA[9], g_macroB[9];
int      g_sb1A, g_sb1B, g_sb2A, g_sb2B, g_sb3A, g_sb3B, g_jdA, g_jdB, g_poAA, g_poAB, g_poMA, g_poMB, g_poDA, g_poDB;


const color C_LIME = C'139,195,74';
bool   EDispMark = false, EShowBos = false, EIfvg = false, EVi = false, EBpr = false, EMit = false, ERb = false, EPb = false, EVac = false;
bool   EZoneMt = false, EPdZone = false, ESessLvl = false, EPm = false, EQual = true, EStats = false;
int    EFvgKeep = 5, EZoneMax = 6, EMaxLiq = 12, EKeepSwept = 20, EStructKeep = 6;
double ELblNear = 0.25, EFvgMin = 0.3;

datetime g_raidT = 0;

//+------------------------------------------------------------------+
//| time zones (MT has no time-zone database, so the rules are coded) |
//+------------------------------------------------------------------+
datetime NthSunday(const int y, const int m, const int n)
  {
   MqlDateTime d; d.year = y; d.mon = m; d.day = 1; d.hour = 0; d.min = 0; d.sec = 0;
   datetime first = StructToTime(d);
   MqlDateTime f; TimeToStruct(first, f);
   int add = (7 - f.day_of_week) % 7;
   return first + (datetime)((add + 7 * (n - 1)) * 86400);
  }

datetime LastSunday(const int y, const int m)
  {
   MqlDateTime d; d.year = (m == 12 ? y + 1 : y); d.mon = (m == 12 ? 1 : m + 1); d.day = 1; d.hour = 0; d.min = 0; d.sec = 0;
   datetime lastDay = StructToTime(d) - 86400;
   MqlDateTime f; TimeToStruct(lastDay, f);
   return lastDay - (datetime)(f.day_of_week * 86400);
  }

bool UsDstUtc(const datetime u)               // 2nd Sunday of March 02:00 EST  ->  1st Sunday of November 02:00 EDT
  {
   MqlDateTime d; TimeToStruct(u, d);
   datetime s = NthSunday(d.year, 3, 2) + 7 * 3600;
   datetime e = NthSunday(d.year, 11, 1) + 6 * 3600;
   return u >= s && u < e;
  }

bool EuDstUtc(const datetime u)               // last Sunday of March 01:00 UTC  ->  last Sunday of October 01:00 UTC
  {
   MqlDateTime d; TimeToStruct(u, d);
   return u >= LastSunday(d.year, 3) + 3600 && u < LastSunday(d.year, 10) + 3600;
  }

bool BrokerDstAt(const datetime u)
  {
   if(InpBrokerDst == TM_DST_US) return UsDstUtc(u);
   if(InpBrokerDst == TM_DST_EU) return EuDstUtc(u);
   return false;
  }

datetime ServerToUtc(const datetime ts)
  {
   datetime summer = ts - (datetime)((InpBrokerGmt + 1) * 3600);
   if(BrokerDstAt(summer)) return summer;
   return ts - (datetime)(InpBrokerGmt * 3600);
  }

datetime ToNY(const datetime u)  { return u + (datetime)(UsDstUtc(u) ? -4 * 3600 : -5 * 3600); }

datetime NyToUtc(const datetime ny)
  {
   datetime u = ny + 5 * 3600;
   if(ToNY(u) != ny) u = ny + 4 * 3600;
   return u;
  }

datetime ToZone(const datetime u, const ENUM_TM_ZONE z)
  {
   if(z == TM_Z_NY) return ToNY(u);
   if(z == TM_Z_UTC) return u;
   if(z == TM_Z_LONDON) return u + (datetime)(EuDstUtc(u) ? 3600 : 0);
   return u + 12600;                           // Tehran: UTC+3:30, no daylight saving
  }

datetime NowUtc()
  {
   datetime g = TimeGMT();
   if(g > 0) return g;
   return ServerToUtc(TimeCurrent());
  }

string Dn() { return ShortToString(0x25BC); }
string Up() { return ShortToString(0x25B2); }
string Ck() { return ShortToString(0x2713); }

string Num2(const double v)
  {
   string t = DoubleToString(v, 2);
   while(StringLen(t) > 0 && StringSubstr(t, StringLen(t) - 1, 1) == "0") t = StringSubstr(t, 0, StringLen(t) - 1);
   if(StringLen(t) > 0 && StringSubstr(t, StringLen(t) - 1, 1) == ".") t = StringSubstr(t, 0, StringLen(t) - 1);
   return t;
  }

string Two(const int v) { return (v < 10 ? "0" : "") + IntegerToString(v); }

string Hm(const datetime clock)
  {
   MqlDateTime d; TimeToStruct(clock, d);
   return Two(d.hour) + ":" + Two(d.min);
  }

string ZoneTag() { return InpTzDisp == TM_Z_NY ? "NY" : (InpTzDisp == TM_Z_UTC ? "UTC" : (InpTzDisp == TM_Z_LONDON ? "LON" : "")); }


//+------------------------------------------------------------------+
//| generic helpers                                                  |
//+------------------------------------------------------------------+
bool IsNa(const double v) { return v == TM_NA; }
int BarSec() { return PeriodSeconds(_Period); }

datetime TimeAt(const int k)
  {
   if(k < 0) return g_t[0];
   if(k <= g_n - 1) return g_t[k];
   return g_t[g_n - 1] + (datetime)((k - (g_n - 1)) * BarSec());
  }

color Tint(const color c, const double alpha)   // blend with the chart background (MT objects have no transparency)
  {
   int cr = (int)(c & 0xFF), cg = (int)((c >> 8) & 0xFF), cb = (int)((c >> 16) & 0xFF);
   int br = (int)(g_bg & 0xFF), bgc = (int)((g_bg >> 8) & 0xFF), bb = (int)((g_bg >> 16) & 0xFF);
   int r = (int)(cr * alpha + br * (1.0 - alpha));
   int g = (int)(cg * alpha + bgc * (1.0 - alpha));
   int b = (int)(cb * alpha + bb * (1.0 - alpha));
   return (color)(r | (g << 8) | (b << 16));
  }

string Fp(const double p) { return DoubleToString(p, _Digits); }

string NextName(const string kind)
  {
   g_cnt++;
   return PFX + kind + IntegerToString(g_cnt);
  }

string MkLine(const string kind, const datetime t1, const double p1, const datetime t2, const double p2, const color c, const int style, const int width)
  {
   string n = NextName(kind);
   ObjectCreate(0, n, OBJ_TREND, 0, t1, p1, t2, p2);
   ObjectSetInteger(0, n, OBJPROP_COLOR, c);
   ObjectSetInteger(0, n, OBJPROP_STYLE, style);
   ObjectSetInteger(0, n, OBJPROP_WIDTH, width);
   ObjectSetInteger(0, n, OBJPROP_RAY_RIGHT, false);
   ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
   return n;
  }

string MkText(const string kind, const datetime t, const double p, const string text, const color c, const int anchor)
  {
   string n = NextName(kind);
   ObjectCreate(0, n, OBJ_TEXT, 0, t, p);
   ObjectSetString(0, n, OBJPROP_TEXT, text);
   ObjectSetInteger(0, n, OBJPROP_COLOR, c);
   ObjectSetInteger(0, n, OBJPROP_FONTSIZE, InpFont);
   ObjectSetInteger(0, n, OBJPROP_ANCHOR, anchor);
   ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
   return n;
  }

string MkRect(const string kind, const datetime t1, const double top, const datetime t2, const double bottom, const color col, const bool fill)
  {
   string n = NextName(kind);
   ObjectCreate(0, n, OBJ_RECTANGLE, 0, t1, top, t2, bottom);
   ObjectSetInteger(0, n, OBJPROP_COLOR, col);
   ObjectSetInteger(0, n, OBJPROP_FILL, fill);
   ObjectSetInteger(0, n, OBJPROP_BACK, true);
   ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
   return n;
  }


//+------------------------------------------------------------------+
//| input parsing, symbol type                                       |
//+------------------------------------------------------------------+
void ParseRange(const string s, const int defA, const int defB, int &a, int &b)
  {
   a = defA; b = defB;
   if(StringLen(s) < 9 || StringSubstr(s, 4, 1) != "-") return;
   int a1 = (int)StringToInteger(StringSubstr(s, 0, 2)), a2 = (int)StringToInteger(StringSubstr(s, 2, 2));
   int b1 = (int)StringToInteger(StringSubstr(s, 5, 2)), b2 = (int)StringToInteger(StringSubstr(s, 7, 2));
   if(a1 < 0 || a1 > 24 || b1 < 0 || b1 > 24 || a2 < 0 || a2 > 59 || b2 < 0 || b2 > 59) return;
   a = a1 * 60 + a2;
   b = b1 * 60 + b2;
   if(b == 1440) b = 0;
  }

bool HasAny(const string s, const string list)    // list separated by ","
  {
   int p = 0;
   while(p < StringLen(list))
     {
      int q = StringFind(list, ",", p);
      if(q < 0) q = StringLen(list);
      string w = StringSubstr(list, p, q - p);
      if(StringLen(w) > 0 && StringFind(s, w) >= 0) return true;
      p = q + 1;
     }
   return false;
  }

void DetectMarket()
  {
   string s = _Symbol;
   StringToUpper(s);
   bool crypto = HasAny(s, "BTC,ETH,XRP,LTC,DOGE,SOL,ADA,BNB,DOT,AVAX,MATIC,TRX,LINK,SHIB");
   bool forex = false;
   if(!crypto && StringLen(s) >= 6)
     {
      string cur = "USD,EUR,GBP,JPY,CHF,CAD,AUD,NZD";
      string a = StringSubstr(s, 0, 3), b = StringSubstr(s, 3, 3);
      forex = (StringFind(cur, a) >= 0 && StringFind(cur, b) >= 0);
     }
   g_isCrypto = (InpMarket == TM_M_CRYPTO) || (InpMarket == TM_M_AUTO && crypto);
   g_isForex  = (InpMarket == TM_M_FOREX)  || (InpMarket == TM_M_AUTO && forex);
  }

//+------------------------------------------------------------------+
//| series: ATR (RMA), New York clock, flags                         |
//+------------------------------------------------------------------+
bool InSess(const int m, const int a, const int b)
  {
   if(a < b) return m >= a && m < b;
   return m >= a || m < b;
  }


//+------------------------------------------------------------------+
//| markers (Raid / Break? / Run / Judas / SMT)                      |
//+------------------------------------------------------------------+
int AddMark(const int bar, const double price, const string text, const color c, const int dir, const int kind)
  {
   double off = 0.0;
   if(kind == 0)                                 // stack ordinary markers that sit close together
     {
      int lvl = 0;
      for(int k = 0; k < g_nMk; k++) if(g_mk[k].alive && g_mk[k].kind == 0 && MathAbs(g_mk[k].bar - bar) <= 8) lvl++;
      off = g_atr[MathMin(bar, g_n - 1)] * 0.5 * MathMin(lvl, 1) * (dir == 1 ? 1 : -1);
     }
   ArrayResize(g_mk, g_nMk + 1);
   g_mkId++;
   g_mk[g_nMk].id = g_mkId; g_mk[g_nMk].bar = bar; g_mk[g_nMk].price = price + off; g_mk[g_nMk].text = text;
   g_mk[g_nMk].col = c; g_mk[g_nMk].dir = dir; g_mk[g_nMk].kind = kind; g_mk[g_nMk].alive = true;
   g_nMk++;
   int cap = (kind == 0) ? 24 : EMaxLiq + 10;     // keep the newest ones only
   int cnt = 0;
   for(int k = 0; k < g_nMk; k++) if(g_mk[k].alive && g_mk[k].kind == kind) cnt++;
   for(int k = 0; k < g_nMk && cnt > cap; k++)
      if(g_mk[k].alive && g_mk[k].kind == kind) { g_mk[k].alive = false; cnt--; }
   return g_mkId;
  }

void KillMark(const int id)
  {
   for(int k = 0; k < g_nMk; k++) if(g_mk[k].id == id) g_mk[k].alive = false;
  }

void SetMarkText(const int id, const string text)
  {
   for(int k = 0; k < g_nMk; k++) if(g_mk[k].id == id) g_mk[k].text = text;
  }

//+------------------------------------------------------------------+
//| liquidity levels                                                 |
//+------------------------------------------------------------------+
void RemoveLq(const int idx)
  {
   for(int j = idx; j < g_nLq - 1; j++) g_lq[j] = g_lq[j + 1];
   g_nLq--;
  }

void AddLiq(const double px, const string nm, const int d, const int kind, const int ob, const int i)
  {
   if(IsNa(px)) return;
   for(int k = g_nLq - 1; k >= 0; k--)
      if(g_lq[k].name == nm)
        {
         if(g_lq[k].pend && g_lq[k].mk >= 0) KillMark(g_lq[k].mk);
         RemoveLq(k);
        }
   while(g_nLq >= EMaxLiq)                      // make room: the oldest level NOT waiting for its verdict goes first
     {
      int idx = -1;
      for(int k = 0; k < g_nLq && idx < 0; k++) if(!g_lq[k].pend) idx = k;
      if(idx < 0) break;
      RemoveLq(idx);
     }
   ArrayResize(g_lq, g_nLq + 1);
   g_lq[g_nLq].price = px; g_lq[g_nLq].name = nm; g_lq[g_nLq].dir = d; g_lq[g_nLq].kind = kind; g_lq[g_nLq].bar = i; g_lq[g_nLq].ob = ob;
   g_lq[g_nLq].swept = false; g_lq[g_nLq].sweptBar = 0; g_lq[g_nLq].pend = false; g_lq[g_nLq].hitBar = 0; g_lq[g_nLq].hitExt = 0.0; g_lq[g_nLq].mk = -1;
   g_nLq++;
  }

int KindScore(const int k) { return (k == 3 || k == 4) ? 4 : (k == 2 ? 3 : (k == 1 ? 2 : 1)); }

void ConfirmRaid(const int qi, const double ext, const int i)
  {
   int sc = KindScore(g_lq[qi].kind);
   double pen = MathAbs(ext - g_lq[qi].price);
   bool sameBar = (g_raidScBar == i);
   bool better = !sameBar || sc > g_raidSc || (sc == g_raidSc && pen > g_raidPen);
   if(g_lq[qi].mk >= 0) { KillMark(g_lq[qi].mk); g_lq[qi].mk = -1; }
   if(!better) return;
   if(sameBar && g_raidMk >= 0) { KillMark(g_raidMk); g_raidMk = -1; }
   g_raidDir = (g_lq[qi].dir == 1) ? -1 : 1;
   g_raidBar = i; g_raidT = g_t[i]; g_raidPx = ext; g_raidName = g_lq[qi].name; g_raidRej = true;
   g_raidSc = sc; g_raidPen = pen; g_raidScBar = i; g_evRaid = g_raidDir;
   if(InpRaidLbl) g_raidMk = AddMark(i, g_lq[qi].dir == 1 ? g_h[i] : g_l[i], "Raid " + g_lq[qi].name, C_ORANGE, g_lq[qi].dir == 1 ? 1 : -1, 1);
  }

void ChkLiq(const int i)
  {
   for(int k = g_nLq - 1; k >= 0; k--)
     {
      if(!g_lq[k].swept)
        {
         if(i > g_lq[k].bar)
           {
            bool hit = g_lq[k].dir == 1 ? g_h[i] > g_lq[k].price : g_l[i] < g_lq[k].price;
            if(hit)
              {
               g_lq[k].swept = true; g_lq[k].sweptBar = i;
               bool rej = g_lq[k].dir == 1 ? g_c[i] < g_lq[k].price : g_c[i] > g_lq[k].price;
               double ext = g_lq[k].dir == 1 ? g_h[i] : g_l[i];
               if(rej) ConfirmRaid(k, ext, i);
               else
                 {
                  g_lq[k].pend = true; g_lq[k].hitBar = i; g_lq[k].hitExt = ext;      // PENDING: back through = RAID, still beyond = RUN
                  if(InpRaidLbl) g_lq[k].mk = AddMark(i, ext, "Break? " + g_lq[k].name, C_GRAY, g_lq[k].dir == 1 ? 1 : -1, 1);
                 }
              }
           }
        }
      else
        {
         if(g_lq[k].pend)
           {
            g_lq[k].hitExt = g_lq[k].dir == 1 ? MathMax(g_lq[k].hitExt, g_h[i]) : MathMin(g_lq[k].hitExt, g_l[i]);
            bool back = g_lq[k].dir == 1 ? g_c[i] < g_lq[k].price : g_c[i] > g_lq[k].price;
            if(back)
              {
               g_lq[k].pend = false;
               ConfirmRaid(k, g_lq[k].hitExt, i);
              }
            else if(i - g_lq[k].hitBar >= InpPendBars)
              {
               g_lq[k].pend = false;
               g_evRun = g_lq[k].dir;
               if(g_lq[k].mk >= 0) SetMarkText(g_lq[k].mk, "Run " + g_lq[k].name);
              }
           }
         if(!g_lq[k].pend && i - g_lq[k].sweptBar > EKeepSwept) RemoveLq(k);
        }
     }
   if(g_raidRej && i - g_raidBar <= InpMssWin)
     {
      if(g_raidDir == 1)  g_raidPx = MathMin(IsNa(g_raidPx) ? g_l[i] : g_raidPx, g_l[i]);
      if(g_raidDir == -1) g_raidPx = MathMax(IsNa(g_raidPx) ? g_h[i] : g_raidPx, g_h[i]);
     }
  }


bool IsSunday(const int i) { return !g_isCrypto && g_dow[i] == 1; }

bool NewWeek(const int i)
  {
   if(i <= 0) return false;
   int a = g_isCrypto ? (g_dow[i] + 5) % 7 : g_dow[i] - 1;
   int b = g_isCrypto ? (g_dow[i - 1] + 5) % 7 : g_dow[i - 1] - 1;
   return a < b;
  }

void PeriodsBar(const int i)
  {
   bool newDay = i > 0 && g_day[i] != g_day[i - 1] && !IsSunday(i);
   bool newWk  = NewWeek(i);
   bool newMth = i > 0 && g_mon[i] != g_mon[i - 1];
   if(newDay)
     {
      if(!IsNa(g_curDH) && InpPdOn)
        {
         AddLiq(g_curDH, "PDH", 1, 2, g_curDHB, i);
         AddLiq(g_curDL, "PDL", -1, 2, g_curDLB, i);
        }
      g_curDH = g_h[i]; g_curDL = g_l[i]; g_curDHB = i; g_curDLB = i;
     }
   else if(!IsSunday(i))
     {
      if(IsNa(g_curDH) || g_h[i] > g_curDH) { g_curDH = g_h[i]; g_curDHB = i; }
      if(IsNa(g_curDL) || g_l[i] < g_curDL) { g_curDL = g_l[i]; g_curDLB = i; }
     }
   if(newWk)
     {
      if(!IsNa(g_curWH) && InpPwOn)
        {
         AddLiq(g_curWH, "PWH", 1, 3, g_curWHB, i);
         AddLiq(g_curWL, "PWL", -1, 3, g_curWLB, i);
        }
      g_curWH = g_h[i]; g_curWL = g_l[i]; g_curWHB = i; g_curWLB = i;
     }
   else
     {
      if(IsNa(g_curWH) || g_h[i] > g_curWH) { g_curWH = g_h[i]; g_curWHB = i; }
      if(IsNa(g_curWL) || g_l[i] < g_curWL) { g_curWL = g_l[i]; g_curWLB = i; }
     }
   if(newMth)
     {
      if(!IsNa(g_curMH) && EPm)
        {
         AddLiq(g_curMH, "PMH", 1, 4, g_curMHB, i);
         AddLiq(g_curML, "PML", -1, 4, g_curMLB, i);
        }
      g_curMH = g_h[i]; g_curML = g_l[i]; g_curMHB = i; g_curMLB = i;
     }
   else
     {
      if(IsNa(g_curMH) || g_h[i] > g_curMH) { g_curMH = g_h[i]; g_curMHB = i; }
      if(IsNa(g_curML) || g_l[i] < g_curML) { g_curML = g_l[i]; g_curMLB = i; }
     }
  }


bool PivotHigh(const int c, const int len)
  {
   for(int k = 1; k <= len; k++) if(!(g_h[c] > g_h[c - k]) || !(g_h[c] > g_h[c + k])) return false;
   return true;
  }

bool PivotLow(const int c, const int len)
  {
   for(int k = 1; k <= len; k++) if(!(g_l[c] < g_l[c - k]) || !(g_l[c] < g_l[c + k])) return false;
   return true;
  }


int g_nItem = 0;
TmItem g_item[];
double g_lblP[];
int g_nLblP = 0;

void AddItem(const string text, const color c, const double price)
  {
   ArrayResize(g_item, g_nItem + 1);
   g_item[g_nItem].text = text; g_item[g_nItem].col = c; g_item[g_nItem].price = price;
   g_nItem++;
  }

double Rng150()
  {
   int s = MathMax(0, g_n - 2 - 149);
   double hi = -DBL_MAX, lo = DBL_MAX;
   for(int k = s; k <= g_n - 2; k++) { if(g_h[k] > hi) hi = g_h[k]; if(g_l[k] < lo) lo = g_l[k]; }
   return (hi > lo) ? hi - lo : 0.0;
  }

void FlushItems()                       // right-side labels: sorted by price, never closer than the minimum gap
  {
   g_nLblP = 0;
   if(g_nItem <= 0) return;
   int ord[];
   ArrayResize(ord, g_nItem);
   for(int k = 0; k < g_nItem; k++) ord[k] = k;
   for(int a = 1; a < g_nItem; a++)     // insertion sort, price descending
     {
      int v = ord[a], b = a - 1;
      while(b >= 0 && g_item[ord[b]].price < g_item[v].price) { ord[b + 1] = ord[b]; b--; }
      ord[b + 1] = v;
     }
   double gap = Rng150() * InpLblGap / 100.0;
   double prevY = TM_NA;
   datetime tx = TimeAt(g_n - 1 + InpLblPad);
   for(int k = 0; k < g_nItem; k++)
     {
      int ix = ord[k];
      double y = g_item[ix].price;
      if(!IsNa(prevY) && prevY - y < gap) y = prevY - gap;
      MkText("RL", tx, y, g_item[ix].text, g_item[ix].col, ANCHOR_LEFT);
      ArrayResize(g_lblP, g_nLblP + 1); g_lblP[g_nLblP] = y; g_nLblP++;
      prevY = y;
     }
   g_nItem = 0;
  }


const int PN_ROWS = 8, PN_M = 14;
int g_pnW = 430, g_pnRH = 18, g_pnKeyW = 150;
string g_pk[10], g_pv[10];
color  g_pc[10];

int  g_pnCorner = CORNER_LEFT_UPPER;
long g_pnSig = -1;

int PnH() { return PN_ROWS * g_pnRH + 12; }

int WmHeight() { return InpShowWm ? InpWmBottom + (int)(InpWmSize * 2.3) + 6 : 0; }   // bottom-left space used by the watermark

int PnMarginY(const int corner)           // vertical gap from the chart edge; bottom-left leaves room for the watermark
  {
   if(corner == CORNER_LEFT_LOWER) return PN_M + WmHeight();
   return PN_M;
  }

int FixedCorner()
  {
   if(InpPanelPos == TM_C_TOPLEFT) return CORNER_LEFT_UPPER;
   if(InpPanelPos == TM_C_BOTTOMRIGHT) return CORNER_RIGHT_LOWER;
   if(InpPanelPos == TM_C_BOTTOMLEFT) return CORNER_LEFT_LOWER;
   return CORNER_RIGHT_UPPER;
  }

// how much of the visible chart a panel in this corner would cover (bars + right-hand price labels)
double CornerCover(const int corner, const int W, const int H, const int first, const int vis, const double pmax, const double pmin, const double ppb)
  {
   bool right = (corner == CORNER_RIGHT_UPPER || corner == CORNER_RIGHT_LOWER);
   bool bottom = (corner == CORNER_LEFT_LOWER || corner == CORNER_RIGHT_LOWER);
   int x0 = right ? W - PN_M - g_pnW : PN_M, x1 = x0 + g_pnW;
   int my = PnMarginY(corner);
   int y0 = bottom ? H - my - PnH() : my, y1 = y0 + PnH();
   double sc = 0.0, k = (double)H / (pmax - pmin);
   for(int s = first; s > first - vis && s >= 0; s--)
     {
      int i = g_n - 1 - s;
      if(i < 0 || i >= g_n) continue;
      double bx = (first - s + 0.5) * ppb;
      if(bx < x0 - 2 || bx > x1 + 2) continue;
      double yt = (pmax - g_h[i]) * k, yb = (pmax - g_l[i]) * k;
      if(yb >= y0 && yt <= y1) sc += 1.0;
     }
   if(right)                                   // right-hand level labels live under the right corners
      for(int j = 0; j < g_nLblP; j++)
        {
         double yy = (pmax - g_lblP[j]) * k;
         if(yy >= y0 - 6 && yy <= y1 + 6) sc += 6.0;
        }
   return sc;
  }

int PanelCorner()
  {
   if(InpPanelPos != TM_C_AUTO) return FixedCorner();
   return g_pnCorner;
  }

void ChooseCorner()                         // Auto: pick the emptiest corner, with hysteresis so it does not jump around
  {
   if(InpPanelPos != TM_C_AUTO) { g_pnCorner = FixedCorner(); return; }
   int W = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), H = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   int first = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR), vis = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   double pmax = ChartGetDouble(0, CHART_PRICE_MAX, 0), pmin = ChartGetDouble(0, CHART_PRICE_MIN, 0);
   if(W <= 0 || H <= 0 || vis <= 0 || pmax <= pmin) return;
   long sig = (long)W * 1000003L + H * 10007L + first * 101L + vis + (long)(pmax * 1000.0) % 9973 + g_nLblP * 13L + g_n;
   if(sig == g_pnSig) return;
   g_pnSig = sig;
   double shiftPct = (InpRightMargin > 0) ? (double)MathMin(InpRightMargin, 50) / 100.0 : 0.0;
   double ppb = (double)W * (1.0 - shiftPct) / vis;
   int cs[4];
   cs[0] = CORNER_LEFT_UPPER; cs[1] = CORNER_RIGHT_UPPER; cs[2] = CORNER_LEFT_LOWER; cs[3] = CORNER_RIGHT_LOWER;
   int best = -1; double bs = 1e18, cur = 1e18;
   for(int k = 0; k < 4; k++)
     {
      double sc = CornerCover(cs[k], W, H, first, vis, pmax, pmin, ppb);
      if(cs[k] == g_pnCorner) cur = sc;
      if(sc < bs) { bs = sc; best = cs[k]; }
     }
   if(best >= 0 && cur > bs + 3.0) g_pnCorner = best;      // move only when clearly better
  }

void PanelRow(const int row, const string key, const string val, const color vc)
  {
   g_pk[row] = key; g_pv[row] = (val == "" ? " " : val); g_pc[row] = vc;      // empty text would show the default word "Label"
  }

void PanelMeasure()                         // size the panel from the real text widths (works at any screen scaling)
  {
   TextSetFont("Consolas", -InpFont * 10);
   uint w = 0, h = 0;
   int kw = 0, vw = 0, rh = 0;
   for(int r = 0; r < PN_ROWS; r++)
     {
      TextGetSize(g_pk[r], w, h); if((int)w > kw) kw = (int)w; if((int)h > rh) rh = (int)h;
      TextGetSize(g_pv[r], w, h); if((int)w > vw) vw = (int)w;
     }
   if(kw <= 0 || rh <= 0)                   // measuring not available: rough estimate
     { kw = (int)(InpFont * 8.0 * 20); rh = InpFont * 2; vw = (int)(InpFont * 8.0 * 30); }
   kw = (int)(kw * 1.12); vw = (int)(vw * 1.12);          // safety margin: fonts differ between PCs
   g_pnKeyW = kw + 16;
   g_pnRH = MathMax(18, rh + 4);
   g_pnW = MathMax(260, 10 + g_pnKeyW + vw + 28);
  }

void PanelOrigin(int &x0, int &y0)          // top-left pixel of the panel; every panel object is anchored top-left, so right/bottom corners are computed from the chart size
  {
   int corner = PanelCorner();
   int W = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), H = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   bool right = (corner == CORNER_RIGHT_UPPER || corner == CORNER_RIGHT_LOWER);
   bool bottom = (corner == CORNER_LEFT_LOWER || corner == CORNER_RIGHT_LOWER);
   x0 = right ? W - PN_M - g_pnW : PN_M;
   y0 = bottom ? H - PnMarginY(corner) - PnH() : PnMarginY(corner);
   if(x0 < 0) x0 = 0;
   if(y0 < 0) y0 = 0;
  }

void PanelDrawRow(const int row, const int x0, const int y0)
  {
   int yy = y0 + 6 + row * g_pnRH;
   for(int k = 0; k < 2; k++)
     {
      string n = PFX + "PN" + IntegerToString(row) + "_" + IntegerToString(k);
      if(ObjectFind(0, n) < 0) ObjectCreate(0, n, OBJ_LABEL, 0, 0, 0);
      int xin = (k == 0) ? 10 : 10 + g_pnKeyW;               // x inside the panel
      ObjectSetInteger(0, n, OBJPROP_CORNER, CORNER_LEFT_UPPER);
      ObjectSetInteger(0, n, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
      ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x0 + xin);
      ObjectSetInteger(0, n, OBJPROP_YDISTANCE, yy);
      ObjectSetInteger(0, n, OBJPROP_COLOR, k == 0 ? (row == 0 ? BRAND_GREEN : C_GRAY) : g_pc[row]);
      ObjectSetInteger(0, n, OBJPROP_FONTSIZE, InpFont);
      ObjectSetString(0, n, OBJPROP_FONT, "Consolas");
      ObjectSetString(0, n, OBJPROP_TEXT, k == 0 ? g_pk[row] : g_pv[row]);
      ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, n, OBJPROP_ZORDER, 10);
     }
  }

void PanelBack(const int x0, const int y0)
  {
   string n = PFX + "PNBG";
   if(ObjectFind(0, n) < 0) ObjectCreate(0, n, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, n, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x0);
   ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y0);
   ObjectSetInteger(0, n, OBJPROP_XSIZE, g_pnW);
   ObjectSetInteger(0, n, OBJPROP_YSIZE, PnH());
   ObjectSetInteger(0, n, OBJPROP_BGCOLOR, Tint(BRAND_DARK, 0.92));
   ObjectSetInteger(0, n, OBJPROP_COLOR, Tint(BRAND_GREEN, 0.5));
   ObjectSetInteger(0, n, OBJPROP_BORDER_TYPE, 1);
   ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, n, OBJPROP_BACK, false);
   ObjectSetInteger(0, n, OBJPROP_ZORDER, 9);
  }


void PaintWatermark()
  {
   if(!InpShowWm)
     {
      ObjectDelete(0, PFX + "WM1");
      ObjectDelete(0, PFX + "WM2");
      return;
     }
   long bg = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   int r = (int)(bg & 0xFF), g = (int)((bg >> 8) & 0xFF), b = (int)((bg >> 16) & 0xFF);
   bool light = (r * 299 + g * 587 + b * 114) / 1000 > 128;
   color orgCol = light ? C'30,34,41' : clrWhite;
   TextSetFont("Arial Black", -InpWmSize * 10);            // measure the real text width so the two parts never overlap
   uint tw = 0, th = 0;
   TextGetSize("crypex", tw, th);
   int wCrypex = (tw > 0) ? (int)tw + 4 : (int)(InpWmSize * 7.0);
   string n1 = PFX + "WM1", n2 = PFX + "WM2";
   for(int k = 0; k < 2; k++)
     {
      string n = (k == 0) ? n1 : n2;
      if(ObjectFind(0, n) < 0) ObjectCreate(0, n, OBJ_LABEL, 0, 0, 0);
      ObjectSetInteger(0, n, OBJPROP_CORNER, CORNER_LEFT_LOWER);
      ObjectSetInteger(0, n, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
      ObjectSetInteger(0, n, OBJPROP_XDISTANCE, InpWmGap + (k == 0 ? 0 : wCrypex));
      ObjectSetInteger(0, n, OBJPROP_YDISTANCE, InpWmBottom);
      ObjectSetInteger(0, n, OBJPROP_COLOR, k == 0 ? BRAND_GREEN : orgCol);
      ObjectSetInteger(0, n, OBJPROP_FONTSIZE, InpWmSize);
      ObjectSetString(0, n, OBJPROP_FONT, "Arial Black");
      ObjectSetString(0, n, OBJPROP_TEXT, k == 0 ? "crypex" : ".org");
      ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, n, OBJPROP_ZORDER, 10);
     }
  }


void Notify(const string msg)
  {
   string full = "Crypex ICT Models " + _Symbol + ": " + msg;
   if(InpAlertPopup) Alert(full);
   if(InpAlertPush)  SendNotification(full);
   if(InpAlertMail)  SendMail("Crypex ICT Models", full);
  }


//+------------------------------------------------------------------+
//| Crypex ICT Models: settings resolved from the Preset             |
//+------------------------------------------------------------------+
void ResolvePreset()
  {
   int p = (int)InpPreset;
   if(p == MD_P_CUSTOM)
     {
      EDispMark = InpDispMark; EShowBos = InpShowBos; EIfvg = InpIfvgOn; EVi = InpViOn; EBpr = InpBprOn; EMit = InpMitOn; ERb = InpRbOn;
      EPb = InpPbOn; EVac = InpVacOn; EZoneMt = InpZoneMt; EPdZone = InpPdZoneOn; ESessLvl = InpSessLvlOn; EPm = InpPmOn;
      EQual = InpFvgQual; EStats = InpSigOn && InpShowStats;
      EFvgKeep = InpFvgKeep; EZoneMax = InpZoneMax; EMaxLiq = InpMaxLiq; EKeepSwept = InpKeepSwept; EStructKeep = InpStructKeep;
      ELblNear = InpLblNear; EFvgMin = InpFvgMin;
      return;
     }
   bool cl = (p == MD_P_CLEAN), fu = (p == MD_P_FULL);
   EDispMark = !cl; EShowBos = !cl; EIfvg = !cl; EVi = !cl; EBpr = !cl; EMit = !cl; ERb = !cl; EPb = !cl; EVac = !cl;
   EZoneMt = !cl; EPdZone = !cl; ESessLvl = !cl; EPm = fu; EQual = cl; EStats = InpSigOn && fu;
   EFvgKeep = cl ? 5 : (fu ? 30 : 16);
   EZoneMax = cl ? 6 : (fu ? 40 : 24);
   EMaxLiq = cl ? 12 : (fu ? 60 : 40);
   EKeepSwept = cl ? 20 : (fu ? 100 : 60);
   EStructKeep = cl ? 6 : (fu ? 24 : 14);
   ELblNear = cl ? 0.25 : (fu ? 0.6 : 0.3);
   EFvgMin = cl ? 0.3 : (fu ? 0.05 : 0.1);
  }

//+------------------------------------------------------------------+
//| model structures                                                 |
//+------------------------------------------------------------------+
struct TmSessM   { string name; int a; int b; bool on; color col; bool active; double hi; double lo; int hiBar; int loBar;
                  double fHi; double fLo; int fHiBar; int fLoBar; };
struct TmFvg    { double top; double bot; int dir; int bar; int state; double ext; bool uni; bool bp; string nm; };
struct TmZone   { double top; double bot; int dir; int kind; string nm; int bar; int born; bool touched; bool prop; bool uni; };
struct TmSetup  { int model; int dir; double entry; double sl; double tp; int born; int fillBar; int state; int score; double rr; double cost; int finBar; bool win; };
struct TmStruct { double px; int fromBar; int toBar; string txt; color col; };
struct TmMBox    { int b0; int b1; double top; double bot; string txt; color col; };
struct TmDisp   { int bar; int dir; };

TmSessM   g_ms[5];
TmFvg    g_fvg[];    int g_nFvg = 0;
TmZone   g_zn[];     int g_nZn = 0;
TmSetup  g_su[];     int g_nSu = 0;
TmStruct g_st[];     int g_nSt = 0;
TmMBox    g_mb[];     int g_nMb = 0;
TmDisp   g_dp[];     int g_nDp = 0;
int      g_htf[];

int      g_trend = 0, g_mssDir = 0, g_mssBar = -9999;
datetime g_mssT = 0;
bool     g_swHiX = true, g_swLoX = true;
int      g_dispBullBar = -9999, g_dispBearBar = -9999;
double   g_eq = TM_NA, g_rngTop = TM_NA, g_rngBot = TM_NA, g_oteTop = TM_NA, g_oteBot = TM_NA, g_oteMid = TM_NA, g_oteSl = TM_NA, g_oteTp = TM_NA;
int      g_oteDir = 0;
bool     g_oteFired = false, g_dealOk = false;
int      g_w22Dir = 0, g_w22Bar = 0;
double   g_w22Px = TM_NA;
bool     g_sbFired = false;
int      g_evMss = 0, g_evBrk = 0, g_evFvg = 0, g_evUni = 0, g_evSig = 0;
double   g_nfTop = TM_NA, g_nfBot = TM_NA, g_nfExt = TM_NA;
int      g_nfDir = 0;
double   g_uniTop = TM_NA, g_uniBot = TM_NA, g_uniFar = TM_NA;
int      g_uniDir = 0, g_uniBar = -9999;
bool     g_uniDone = true;
int      g_capRej = 0;
int      g_win[5], g_loss[5];
int      g_sbA[3], g_sbB[3];

string Bolt() { return ShortToString(0x26A1); }
long   AgeSec() { return (long)InpAgeMin * 60; }
int    Cl1400(const int x) { return MathMax(x, g_n - 1 - 1400); }

//+------------------------------------------------------------------+
//| sessions (high / low tracking only; finished ones become levels) |
//+------------------------------------------------------------------+
void InitModels()
  {
   ResolvePreset();
   string nm[5] = {"Asia", "London KZ", "NY AM KZ", "London Close KZ", "NY PM KZ"};
   int    dA[5] = {1200, 120, 420, 600, 810};
   int    dB[5] = {0, 300, 600, 720, 960};
   string rg[5]; bool on[5]; color cl[5];
   rg[0] = InpAsiaR; rg[1] = InpLonR; rg[2] = InpNyaR; rg[3] = InpLcR; rg[4] = InpNypR;
   on[0] = InpAsiaOn; on[1] = InpLonOn; on[2] = InpNyaOn; on[3] = InpLcOn; on[4] = InpNypOn;
   cl[0] = C_PURPLE; cl[1] = C_BLUE; cl[2] = C_BULL; cl[3] = C_ORANGE; cl[4] = C_PINK;
   for(int k = 0; k < 5; k++)
     {
      g_ms[k].name = nm[k]; g_ms[k].on = on[k]; g_ms[k].col = cl[k];
      ParseRange(rg[k], dA[k], dB[k], g_ms[k].a, g_ms[k].b);
     }
   ParseRange(InpSb1R, 180, 240, g_sbA[0], g_sbB[0]);
   ParseRange(InpSb2R, 600, 660, g_sbA[1], g_sbB[1]);
   ParseRange(InpSb3R, 840, 900, g_sbA[2], g_sbB[2]);
  }

bool Fl(const int i, const int bit) { return i >= 0 && (g_fl[i] & (1 << bit)) != 0; }
bool SbAny(const int i)  { return InpSbOn && (Fl(i, 5) || Fl(i, 6) || Fl(i, 7)); }
bool KzAny(const int i)  { return (InpLonOn && Fl(i, 1)) || (InpNyaOn && Fl(i, 2)) || (InpLcOn && Fl(i, 3)) || (InpNypOn && Fl(i, 4)) || SbAny(i); }

bool SessUpd(const int k, const bool inS, const int i)
  {
   bool ended = false;
   if(!g_ms[k].on) return false;
   if(inS)
     {
      if(!g_ms[k].active)
        { g_ms[k].active = true; g_ms[k].hi = g_h[i]; g_ms[k].lo = g_l[i]; g_ms[k].hiBar = i; g_ms[k].loBar = i; }
      else
        {
         if(g_h[i] > g_ms[k].hi) { g_ms[k].hi = g_h[i]; g_ms[k].hiBar = i; }
         if(g_l[i] < g_ms[k].lo) { g_ms[k].lo = g_l[i]; g_ms[k].loBar = i; }
        }
     }
   else if(g_ms[k].active)
     {
      g_ms[k].active = false; ended = true;
      g_ms[k].fHi = g_ms[k].hi; g_ms[k].fLo = g_ms[k].lo; g_ms[k].fHiBar = g_ms[k].hiBar; g_ms[k].fLoBar = g_ms[k].loBar;
     }
   return ended;
  }

void SessEnd(const int k, const int i)
  {
   if(!ESessLvl) return;
   AddLiq(g_ms[k].fHi, g_ms[k].name + " H", 1, 1, g_ms[k].fHiBar, i);
   AddLiq(g_ms[k].fLo, g_ms[k].name + " L", -1, 1, g_ms[k].fLoBar, i);
  }

//+------------------------------------------------------------------+
//| series: ATR, New York clock, session flags, HTF bias             |
//+------------------------------------------------------------------+
void BuildHtf()
  {
   for(int i = 0; i < g_n; i++) g_htf[i] = 0;
   string sym = _Symbol;
   if(PeriodSeconds(InpBiasTF) <= PeriodSeconds(_Period)) return;        // the bias must come from a higher timeframe
   int kOld = iBarShift(sym, InpBiasTF, g_t[0], false);
   if(kOld < 0) return;
   int kMax = kOld + 120;
   while(kMax > 0 && iClose(sym, InpBiasTF, kMax) <= 0.0) kMax--;
   if(kMax < 2) return;
   double ema[];
   ArrayResize(ema, kMax + 1);
   double a = 2.0 / (InpBiasLen + 1.0);
   for(int k = kMax; k >= 0; k--)
     {
      double c = iClose(sym, InpBiasTF, k);
      ema[k] = (k == kMax) ? c : a * c + (1.0 - a) * ema[k + 1];
     }
   for(int i = 0; i < g_n; i++)                                           // previous COMPLETED higher-timeframe bar only: cannot repaint
     {
      int k = iBarShift(sym, InpBiasTF, g_t[i], false);
      if(k < 0) continue;
      int p = k + 1;
      if(p > kMax) continue;
      g_htf[i] = (iClose(sym, InpBiasTF, p) > ema[p]) ? 1 : -1;
     }
  }

void BuildSeries()
  {
   ArrayResize(g_atr, g_n); ArrayResize(g_ny, g_n); ArrayResize(g_mn, g_n); ArrayResize(g_dow, g_n);
   ArrayResize(g_day, g_n); ArrayResize(g_mon, g_n); ArrayResize(g_fl, g_n); ArrayResize(g_htf, g_n);
   double sum = 0.0;
   for(int i = 0; i < g_n; i++)
     {
      double tr = g_h[i] - g_l[i];
      if(i > 0) tr = MathMax(tr, MathMax(MathAbs(g_h[i] - g_c[i - 1]), MathAbs(g_l[i] - g_c[i - 1])));
      if(i < 13) { sum += tr; g_atr[i] = 0.0; }
      else if(i == 13) { sum += tr; g_atr[i] = sum / 14.0; }
      else g_atr[i] = (g_atr[i - 1] * 13.0 + tr) / 14.0;
     }
   for(int i = 0; i < g_n; i++)
     {
      datetime u = ServerToUtc(g_t[i]);
      datetime ny = ToNY(u);
      g_ny[i] = ny;
      MqlDateTime d; TimeToStruct(ny, d);
      g_mn[i] = d.hour * 60 + d.min;
      g_dow[i] = d.day_of_week + 1;
      g_day[i] = d.day;
      g_mon[i] = d.mon;
      int f = 0;
      if(g_intra)
        {
         int m = g_mn[i];
         for(int k = 0; k < 5; k++) if(InSess(m, g_ms[k].a, g_ms[k].b)) f |= (1 << k);
         for(int k = 0; k < 3; k++) if(InSess(m, g_sbA[k], g_sbB[k])) f |= (1 << (5 + k));
        }
      g_fl[i] = f;
     }
   BuildHtf();
  }

//+------------------------------------------------------------------+
//| state                                                            |
//+------------------------------------------------------------------+
void ResetState()
  {
   g_nMk = 0; g_mkId = 0; g_nLq = 0; g_cnt = 0;
   ArrayResize(g_mk, 0); ArrayResize(g_lq, 0);
   g_nFvg = 0; g_nZn = 0; g_nSu = 0; g_nSt = 0; g_nMb = 0; g_nDp = 0;
   ArrayResize(g_fvg, 0); ArrayResize(g_zn, 0); ArrayResize(g_su, 0); ArrayResize(g_st, 0); ArrayResize(g_mb, 0); ArrayResize(g_dp, 0);
   for(int k = 0; k < 5; k++)
     { g_ms[k].active = false; g_ms[k].fHi = TM_NA; g_ms[k].fLo = TM_NA; g_ms[k].hi = TM_NA; g_ms[k].lo = TM_NA; g_ms[k].hiBar = 0; g_ms[k].loBar = 0; g_ms[k].fHiBar = 0; g_ms[k].fLoBar = 0; }
   g_swHi = TM_NA; g_pSwHi = TM_NA; g_swLo = TM_NA; g_pSwLo = TM_NA; g_swHiBar = 0; g_swLoBar = 0;
   g_swHiX = true; g_swLoX = true; g_trend = 0; g_mssDir = 0; g_mssBar = -9999; g_mssT = 0;
   g_dispBullBar = -9999; g_dispBearBar = -9999;
   g_raidDir = 0; g_raidBar = -9999; g_raidT = 0; g_raidSc = 0; g_raidScBar = -1; g_raidMk = -1; g_raidPx = TM_NA; g_raidPen = 0.0; g_raidName = ""; g_raidRej = false;
   g_curDH = TM_NA; g_curDL = TM_NA; g_curWH = TM_NA; g_curWL = TM_NA; g_curMH = TM_NA; g_curML = TM_NA;
   g_curDHB = 0; g_curDLB = 0; g_curWHB = 0; g_curWLB = 0; g_curMHB = 0; g_curMLB = 0;
   g_jdDone = false; g_po3Bias = 0; g_po3DnBar = -9999; g_po3UpBar = -9999; g_po3DnExt = TM_NA; g_po3UpExt = TM_NA; g_moPx = TM_NA;
   g_eq = TM_NA; g_rngTop = TM_NA; g_rngBot = TM_NA; g_oteDir = 0; g_oteTop = TM_NA; g_oteBot = TM_NA; g_oteMid = TM_NA; g_oteSl = TM_NA; g_oteTp = TM_NA;
   g_oteFired = false; g_dealOk = false;
   g_w22Dir = 0; g_w22Bar = 0; g_w22Px = TM_NA; g_sbFired = false;
   g_nfTop = TM_NA; g_nfBot = TM_NA; g_nfExt = TM_NA; g_nfDir = 0;
   g_uniTop = TM_NA; g_uniBot = TM_NA; g_uniFar = TM_NA; g_uniDir = 0; g_uniBar = -9999; g_uniDone = true;
   g_capRej = 0;
   for(int k = 0; k < 5; k++) { g_win[k] = 0; g_loss[k] = 0; }
   g_evRaid = 0; g_evRun = 0; g_evMss = 0; g_evBrk = 0; g_evFvg = 0; g_evUni = 0; g_evSig = 0;
  }

//+------------------------------------------------------------------+
//| displacement                                                     |
//+------------------------------------------------------------------+
bool DispBull(const int i)
  {
   double rg = g_h[i] - g_l[i], bd = MathAbs(g_c[i] - g_o[i]);
   return g_c[i] > g_o[i] && rg > 0 && g_atr[i] > 0 && bd >= InpDispAtr * g_atr[i] && bd / rg >= InpDispBody;
  }
bool DispBear(const int i)
  {
   double rg = g_h[i] - g_l[i], bd = MathAbs(g_c[i] - g_o[i]);
   return g_c[i] < g_o[i] && rg > 0 && g_atr[i] > 0 && bd >= InpDispAtr * g_atr[i] && bd / rg >= InpDispBody;
  }
bool MidBull(const int k) { return k >= 0 && g_atr[k] > 0 && g_c[k] > g_o[k] && MathAbs(g_c[k] - g_o[k]) >= InpFvgMid * g_atr[k]; }
bool MidBear(const int k) { return k >= 0 && g_atr[k] > 0 && g_c[k] < g_o[k] && MathAbs(g_c[k] - g_o[k]) >= InpFvgMid * g_atr[k]; }

void AddDisp(const int bar, const int dir)
  {
   ArrayResize(g_dp, g_nDp + 1);
   g_dp[g_nDp].bar = bar; g_dp[g_nDp].dir = dir; g_nDp++;
   while(g_nDp > 12) { for(int j = 0; j < g_nDp - 1; j++) g_dp[j] = g_dp[j + 1]; g_nDp--; }
  }

//+------------------------------------------------------------------+
//| zones (blocks)                                                   |
//+------------------------------------------------------------------+
int AddZone(const double top, const double bot, const int d, const int kind, const int bar, const string nm, const int i)
  {
   bool ok = (kind == 1 && InpObOn) || (kind == 2 && InpBrkOn) || (kind == 3 && EMit) || (kind == 4 && ERb) || (kind == 5 && EPb) || (kind == 6 && EVac);
   if(!ok || !(top > bot)) return -1;
   for(int q = 0; q < g_nZn; q++)                   // the same candle can give the same block twice (repeated breaks of one swing): keep one
      if(g_zn[q].kind == kind && g_zn[q].dir == d && g_zn[q].bar == bar) return q;
   while(g_nZn >= EZoneMax) { for(int j = 0; j < g_nZn - 1; j++) g_zn[j] = g_zn[j + 1]; g_nZn--; }
   ArrayResize(g_zn, g_nZn + 1);
   g_zn[g_nZn].top = top; g_zn[g_nZn].bot = bot; g_zn[g_nZn].dir = d; g_zn[g_nZn].kind = kind; g_zn[g_nZn].nm = nm;
   g_zn[g_nZn].bar = bar; g_zn[g_nZn].born = i; g_zn[g_nZn].touched = false; g_zn[g_nZn].prop = false; g_zn[g_nZn].uni = false;
   g_nZn++;
   return g_nZn - 1;
  }

int FindOff(const int cBar, const bool wantBear, const int span, const int i)
  {
   int base = i - cBar, res = base;
   bool found = false;
   for(int k = 0; k <= span; k++)
     {
      int o = base + k;
      if(o <= 1000 && !found)
        {
         int idx = i - o;
         if(idx < 0) continue;
         bool isb = g_c[idx] < g_o[idx];
         if(isb == wantBear) { res = o; found = true; }
        }
     }
   return res;
  }

void BlockFromIdx(const int idx, const int d, double &t, double &b)
  {
   t = g_h[idx]; b = g_l[idx];
   if(InpZoneMode == MD_Z_BODY) { t = MathMax(g_o[idx], g_c[idx]); b = MathMin(g_o[idx], g_c[idx]); }
   if(InpZoneMode == MD_Z_OPEN)
     {
      if(d == 1) t = MathMax(g_o[idx], g_c[idx]);
      else b = MathMin(g_o[idx], g_c[idx]);
     }
  }

void ZoneAt(const int cBar, const bool wantBear, const int d, const int kind, const string nm, const int i)
  {
   if(i - cBar > 900) return;
   int o = FindOff(cBar, wantBear, 3, i);
   int idx = i - o;
   if(idx < 0) return;
   double t, b;
   BlockFromIdx(idx, d, t, b);
   AddZone(t, b, d, kind, idx, nm, i);
  }

void UpdZones(const int i)
  {
   double pqT[], pqB[]; int pqD[]; int np = 0;
   for(int k = g_nZn - 1; k >= 0; k--)
     {
      bool dead = false;
      if(g_zn[k].kind == 6) dead = (g_zn[k].dir == 1) ? g_l[i] <= g_zn[k].bot : g_h[i] >= g_zn[k].top;
      else dead = (g_zn[k].dir == 1) ? g_c[i] < g_zn[k].bot : g_c[i] > g_zn[k].top;
      if(dead)
        {
         for(int j = k; j < g_nZn - 1; j++) g_zn[j] = g_zn[j + 1];
         g_nZn--;
         continue;
        }
      if(!g_zn[k].touched)
        {
         bool tch = (g_zn[k].dir == 1) ? g_l[i] <= g_zn[k].top : g_h[i] >= g_zn[k].bot;
         if(tch && i > g_zn[k].bar) g_zn[k].touched = true;
        }
      if(EPb && !g_zn[k].prop && g_zn[k].kind >= 1 && g_zn[k].kind <= 3 && i > g_zn[k].bar)
        {
         if(g_zn[k].dir == 1 && g_c[i] > g_o[i] && g_l[i] <= g_zn[k].top && g_l[i] >= g_zn[k].bot && g_c[i] > g_zn[k].top)
           {
            g_zn[k].prop = true;
            ArrayResize(pqT, np + 1); ArrayResize(pqB, np + 1); ArrayResize(pqD, np + 1);
            pqT[np] = MathMin(g_o[i], g_zn[k].top); pqB[np] = g_l[i]; pqD[np] = 1; np++;
           }
         if(g_zn[k].dir == -1 && g_c[i] < g_o[i] && g_h[i] >= g_zn[k].bot && g_h[i] <= g_zn[k].top && g_c[i] < g_zn[k].bot)
           {
            g_zn[k].prop = true;
            ArrayResize(pqT, np + 1); ArrayResize(pqB, np + 1); ArrayResize(pqD, np + 1);
            pqT[np] = g_h[i]; pqB[np] = MathMax(g_o[i], g_zn[k].bot); pqD[np] = -1; np++;
           }
        }
     }
   for(int q = 0; q < np; q++)
      AddZone(pqT[q], pqB[q], pqD[q], 5, i, pqD[q] == 1 ? "Bull Propulsion" : "Bear Propulsion", i);
  }

void Gaps(const int i)
  {
   if(!EVac || i <= 0 || g_atr[i] <= 0) return;
   double g = g_o[i] - g_c[i - 1];
   if(MathAbs(g) < InpGapAtr * g_atr[i]) return;
   bool wkndGap = g_intra && (g_t[i] - g_t[i - 1]) >= 24 * 3600;
   bool dayChg = g_day[i] != g_day[i - 1];
   bool ndog = g_intra && g_mn[i] >= 1020 && g_mn[i - 1] < 1020 && !dayChg;
   string nm = wkndGap ? "NWOG" : (ndog ? "NDOG" : "Vacuum");
   AddZone(MathMax(g_o[i], g_c[i - 1]), MathMin(g_o[i], g_c[i - 1]), g > 0 ? 1 : -1, 6, i - 1, nm, i);
  }

//+------------------------------------------------------------------+
//| fair value gaps                                                  |
//+------------------------------------------------------------------+
void AddMisc(const int b0, const int b1, const double top, const double bot, const string txt, const color c)
  {
   ArrayResize(g_mb, g_nMb + 1);
   g_mb[g_nMb].b0 = b0; g_mb[g_nMb].b1 = b1; g_mb[g_nMb].top = top; g_mb[g_nMb].bot = bot; g_mb[g_nMb].txt = txt; g_mb[g_nMb].col = c;
   g_nMb++;
   while(g_nMb > 30) { for(int j = 0; j < g_nMb - 1; j++) g_mb[j] = g_mb[j + 1]; g_nMb--; }
  }

int NewFvg(const double top, const double bot, const int d, const double ext, const int i)
  {
   while(g_nFvg >= EFvgKeep) { for(int j = 0; j < g_nFvg - 1; j++) g_fvg[j] = g_fvg[j + 1]; g_nFvg--; }
   ArrayResize(g_fvg, g_nFvg + 1);
   g_fvg[g_nFvg].top = top; g_fvg[g_nFvg].bot = bot; g_fvg[g_nFvg].dir = d; g_fvg[g_nFvg].bar = i; g_fvg[g_nFvg].state = 0;
   g_fvg[g_nFvg].ext = ext; g_fvg[g_nFvg].uni = false; g_fvg[g_nFvg].bp = false; g_fvg[g_nFvg].nm = "FVG";
   g_nFvg++;
   return g_nFvg - 1;
  }

void FvgBpr(const int fi, const int dirWanted, const double minSz, const int i)
  {
   if(!EBpr) return;
   for(int k = 0; k < g_nFvg; k++)
     {
      if(k == fi) continue;
      if(g_fvg[k].dir == dirWanted && g_fvg[k].state == 0 && !g_fvg[k].bp && i - g_fvg[k].bar <= 40)
        {
         double oT = MathMin(g_fvg[fi].top, g_fvg[k].top), oB = MathMax(g_fvg[fi].bot, g_fvg[k].bot);
         if(oT - oB >= minSz)
           {
            g_fvg[fi].bp = true; g_fvg[k].bp = true;
            AddMisc(MathMin(g_fvg[k].bar, i) - 2, i + 6, oT, oB, "BPR", C_PURPLE);
           }
        }
     }
  }

void FvgDetect(const int i)
  {
   if(!InpFvgOn || i < 2 || g_atr[i] <= 0) return;
   double atr = g_atr[i], minSz = EFvgMin * atr;
   if(g_l[i] > g_h[i - 2] && (g_l[i] - g_h[i - 2]) >= minSz && (!EQual || (MidBull(i - 1) && g_c[i - 1] > g_h[i - 2])))
     {
      int fi = NewFvg(g_l[i], g_h[i - 2], 1, MathMin(g_l[i], MathMin(g_l[i - 1], g_l[i - 2])), i);
      g_evFvg = 1; g_nfTop = g_fvg[fi].top; g_nfBot = g_fvg[fi].bot; g_nfExt = g_fvg[fi].ext; g_nfDir = 1;
      FvgBpr(fi, -1, minSz, i);
     }
   if(g_h[i] < g_l[i - 2] && (g_l[i - 2] - g_h[i]) >= minSz && (!EQual || (MidBear(i - 1) && g_c[i - 1] < g_l[i - 2])))
     {
      int fi = NewFvg(g_l[i - 2], g_h[i], -1, MathMax(g_h[i], MathMax(g_h[i - 1], g_h[i - 2])), i);
      g_evFvg = -1; g_nfTop = g_fvg[fi].top; g_nfBot = g_fvg[fi].bot; g_nfExt = g_fvg[fi].ext; g_nfDir = -1;
      FvgBpr(fi, 1, minSz, i);
     }
   if(EVi)                                        // volume imbalance (bodies gap, wicks overlap)
     {
      double vs = 0.05 * atr;
      if(MathMin(g_o[i], g_c[i]) - MathMax(g_o[i - 1], g_c[i - 1]) >= vs && g_l[i] <= g_h[i - 1])
         AddMisc(i - 1, i + 8, MathMin(g_o[i], g_c[i]), MathMax(g_o[i - 1], g_c[i - 1]), "VI", C_BULL);
      if(MathMin(g_o[i - 1], g_c[i - 1]) - MathMax(g_o[i], g_c[i]) >= vs && g_h[i] >= g_l[i - 1])
         AddMisc(i - 1, i + 8, MathMin(g_o[i - 1], g_c[i - 1]), MathMax(g_o[i], g_c[i]), "VI", C_BEAR);
     }
  }

void FvgUpdate(const int i)
  {
   for(int k = g_nFvg - 1; k >= 0; k--)
     {
      bool kill = false;
      if(i > g_fvg[k].bar)
        {
         if(g_fvg[k].state == 0)
           {
            if(g_fvg[k].dir == 1)                 // bullish gap is filled from above
              {
               if(g_l[i] < g_fvg[k].bot)
                 {
                  if(g_c[i] < g_fvg[k].bot && EIfvg) { g_fvg[k].state = 1; g_fvg[k].dir = -1; g_fvg[k].nm = "IFVG"; }
                  else kill = true;
                 }
               else if(InpFvgShrink && g_l[i] < g_fvg[k].top) g_fvg[k].top = MathMax(g_l[i], g_fvg[k].bot);
              }
            else                                  // bearish gap is filled from below
              {
               if(g_h[i] > g_fvg[k].top)
                 {
                  if(g_c[i] > g_fvg[k].top && EIfvg) { g_fvg[k].state = 1; g_fvg[k].dir = 1; g_fvg[k].nm = "IFVG"; }
                  else kill = true;
                 }
               else if(InpFvgShrink && g_h[i] > g_fvg[k].bot) g_fvg[k].bot = MathMin(g_h[i], g_fvg[k].top);
              }
           }
         else
           {
            if(g_fvg[k].dir == -1 && g_c[i] > g_fvg[k].top) kill = true;       // inverted FVG dies when price closes back through
            if(g_fvg[k].dir == 1 && g_c[i] < g_fvg[k].bot) kill = true;
           }
        }
      if(kill)
        {
         for(int j = k; j < g_nFvg - 1; j++) g_fvg[j] = g_fvg[j + 1];
         g_nFvg--;
        }
     }
  }

//+------------------------------------------------------------------+
//| unicorn                                                          |
//+------------------------------------------------------------------+
void Unicorn(const int i)
  {
   if(!InpUniOn) return;
   for(int a = 0; a < g_nZn; a++)
     {
      if(!(g_zn[a].kind == 2 && !g_zn[a].uni && g_mssDir == g_zn[a].dir && g_zn[a].born >= g_mssBar && (g_t[i] - g_mssT) <= AgeSec())) continue;
      for(int b = 0; b < g_nFvg; b++)
        {
         if(g_zn[a].uni) break;
         if(!(!g_fvg[b].uni && g_fvg[b].state == 0 && g_fvg[b].dir == g_zn[a].dir && g_fvg[b].bar >= g_mssBar - 3 && g_fvg[b].bar <= g_mssBar + InpUniWin)) continue;
         double oT = MathMin(g_zn[a].top, g_fvg[b].top), oB = MathMax(g_zn[a].bot, g_fvg[b].bot);
         if(oT > oB)
           {
            g_zn[a].uni = true; g_fvg[b].uni = true;
            AddMisc(MathMin(g_zn[a].bar, g_fvg[b].bar), i + 10, oT, oB, "Unicorn", g_fvg[b].dir == 1 ? C_CYAN : C_ORANGE);
            g_evUni = g_fvg[b].dir; g_uniTop = oT; g_uniBot = oB; g_uniDir = g_fvg[b].dir; g_uniBar = i; g_uniDone = false;
            g_uniFar = g_fvg[b].dir == 1 ? g_zn[a].bot : g_zn[a].top;
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| structure: swings, dealing range, MSS / BOS                      |
//+------------------------------------------------------------------+
void ComputeDealing()
  {
   g_dealOk = false;
   if(IsNa(g_swHi) || IsNa(g_swLo) || !(g_swHi > g_swLo)) return;
   g_dealOk = true;
   double r = g_swHi - g_swLo;
   g_eq = g_swLo + r / 2.0; g_rngTop = g_swHi; g_rngBot = g_swLo;
   g_oteFired = false;
   if(g_swLoBar < g_swHiBar)
     {
      g_oteDir = 1; g_oteTop = g_swHi - InpOteLo * r; g_oteBot = g_swHi - InpOteHi * r; g_oteMid = g_swHi - 0.705 * r;
      g_oteSl = g_swLo; g_oteTp = g_swHi + 0.27 * r;
     }
   else
     {
      g_oteDir = -1; g_oteBot = g_swLo + InpOteLo * r; g_oteTop = g_swLo + InpOteHi * r; g_oteMid = g_swLo + 0.705 * r;
      g_oteSl = g_swHi; g_oteTp = g_swLo - 0.27 * r;
     }
  }

void PivotsM(const int i)
  {
   int len = InpSwLen, c = i - len;
   if(c - len < 0) return;
   bool ph = PivotHigh(c, len), pl = PivotLow(c, len), changed = false;
   double atr = g_atr[i];
   if(ph)
     {
      g_pSwHi = g_swHi; g_swHi = g_h[c]; g_swHiBar = c; g_swHiX = false; changed = true;
      if(InpEqOn && !IsNa(g_pSwHi) && atr > 0 && MathAbs(g_swHi - g_pSwHi) <= InpEqTol * atr)
         AddLiq(MathMax(g_swHi, g_pSwHi), "EQH", 1, 5, g_swHiBar, i);
      if(ERb)
        {
         double wk = g_h[c] - MathMax(g_o[c], g_c[c]), cr = g_h[c] - g_l[c];
         if(cr > 0 && wk >= InpRbPct * cr && wk >= InpRbAtr * atr) AddZone(g_h[c], MathMax(g_o[c], g_c[c]), -1, 4, c, "Bear Rejection", i);
        }
     }
   if(pl)
     {
      g_pSwLo = g_swLo; g_swLo = g_l[c]; g_swLoBar = c; g_swLoX = false; changed = true;
      if(InpEqOn && !IsNa(g_pSwLo) && atr > 0 && MathAbs(g_swLo - g_pSwLo) <= InpEqTol * atr)
         AddLiq(MathMin(g_swLo, g_pSwLo), "EQL", -1, 5, g_swLoBar, i);
      if(ERb)
        {
         double wk = MathMin(g_o[c], g_c[c]) - g_l[c], cr = g_h[c] - g_l[c];
         if(cr > 0 && wk >= InpRbPct * cr && wk >= InpRbAtr * atr) AddZone(MathMin(g_o[c], g_c[c]), g_l[c], 1, 4, c, "Bull Rejection", i);
        }
     }
   if(changed) ComputeDealing();
  }

void AddStruct(const double px, const int fromBar, const int toBar, const string txt, const color c)
  {
   ArrayResize(g_st, g_nSt + 1);
   g_st[g_nSt].px = px; g_st[g_nSt].fromBar = fromBar; g_st[g_nSt].toBar = toBar; g_st[g_nSt].txt = txt; g_st[g_nSt].col = c;
   g_nSt++;
   while(g_nSt > EStructKeep) { for(int j = 0; j < g_nSt - 1; j++) g_st[j] = g_st[j + 1]; g_nSt--; }
  }

void Breaks(const int i)
  {
   if(!IsNa(g_swHi) && !g_swHiX && g_c[i] > g_swHi)
     {
      g_swHiX = true; g_evBrk = 1;
      bool dOk = i - g_dispBullBar <= 3;
      bool isMss = g_trend == -1 && (dOk || !InpMssNeedD);
      string tx = isMss ? (dOk ? "MSS " + Bolt() : "MSS") : (g_trend == -1 ? "CHoCH" : "BOS");
      if((isMss && InpShowMss) || (!isMss && EShowBos)) AddStruct(g_swHi, g_swHiBar, i, tx, C_BULL);
      if(isMss)
        {
         g_mssDir = 1; g_mssBar = i; g_mssT = g_t[i]; g_evMss = 1;
         bool swept = !IsNa(g_pSwLo) && !IsNa(g_swLo) && g_swLo < g_pSwLo;
         if(swept) ZoneAt(g_swHiBar, false, 1, 2, "Bull Breaker", i);
         else ZoneAt(g_swLoBar, true, 1, 3, "Bull Mitigation", i);
        }
      else if(g_trend == 1 || g_trend == 0) ZoneAt(g_swLoBar, true, 1, 1, "Bull OB", i);
      g_trend = 1;
     }
   if(!IsNa(g_swLo) && !g_swLoX && g_c[i] < g_swLo)
     {
      g_swLoX = true; g_evBrk = -1;
      bool dOk = i - g_dispBearBar <= 3;
      bool isMss = g_trend == 1 && (dOk || !InpMssNeedD);
      string tx = isMss ? (dOk ? "MSS " + Bolt() : "MSS") : (g_trend == 1 ? "CHoCH" : "BOS");
      if((isMss && InpShowMss) || (!isMss && EShowBos)) AddStruct(g_swLo, g_swLoBar, i, tx, C_BEAR);
      if(isMss)
        {
         g_mssDir = -1; g_mssBar = i; g_mssT = g_t[i]; g_evMss = -1;
         bool swept = !IsNa(g_pSwHi) && !IsNa(g_swHi) && g_swHi > g_pSwHi;
         if(swept) ZoneAt(g_swLoBar, true, -1, 2, "Bear Breaker", i);
         else ZoneAt(g_swHiBar, false, -1, 3, "Bear Mitigation", i);
        }
      else if(g_trend == -1 || g_trend == 0) ZoneAt(g_swHiBar, false, -1, 1, "Bear OB", i);
      g_trend = -1;
     }
  }

//+------------------------------------------------------------------+
//| models and setups                                                |
//+------------------------------------------------------------------+
string ModelName(const int m) { return m == 1 ? "ICT 2022" : (m == 2 ? "Silver Bullet" : (m == 3 ? "Unicorn" : "OTE")); }

int Score(const int d, const bool kz, const int i)
  {
   int s = 0;
   if(kz) s++;
   if(g_raidDir == d && (g_t[i] - g_raidT) <= AgeSec()) s++;
   if(g_mssDir == d && (g_t[i] - g_mssT) <= AgeSec()) s++;
   if((d == 1 && i - g_dispBullBar <= 20) || (d == -1 && i - g_dispBearBar <= 20)) s++;
   if(g_htf[i] == d) s++;
   if(!IsNa(g_eq) && ((d == 1 && g_c[i] < g_eq) || (d == -1 && g_c[i] > g_eq))) s++;
   return s;
  }

double TargetPx(const int d, const double entry, const double minDist)
  {
   double best = TM_NA;
   for(int k = 0; k < g_nLq; k++)
     {
      if(g_lq[k].swept || g_lq[k].dir != d) continue;
      if(d == 1 && g_lq[k].price >= entry + minDist && (IsNa(best) || g_lq[k].price < best)) best = g_lq[k].price;
      if(d == -1 && g_lq[k].price <= entry - minDist && (IsNa(best) || g_lq[k].price > best)) best = g_lq[k].price;
     }
   return best;
  }

bool NewSetup(const int model, const int d, const double entry, const double sl, const bool inKz, const bool filled, const int i)
  {
   if(!InpSigOn) return false;
   double risk = MathAbs(entry - sl);
   bool dirOk = (d == 1) ? sl < entry : sl > entry;
   int sc = Score(d, inKz, i);
   bool htfOk = !InpHtfReq || g_htf[i] == d;
   if(!(risk > 0 && dirOk && htfOk && sc >= InpMinScore)) return false;
   double tp = TargetPx(d, entry, risk * InpMinRR);
   if(IsNa(tp)) tp = entry + d * risk * InpFbRR;
   double rr = MathAbs(tp - entry) / risk;
   if(rr < InpMinRR - 0.0001) return false;
   int oi = 0;                                    // only FINISHED setups may be dropped; open / pending trades never are
   while(g_nSu >= 24 && oi < g_nSu)
     {
      if(g_su[oi].state == 2) { for(int j = oi; j < g_nSu - 1; j++) g_su[j] = g_su[j + 1]; g_nSu--; }
      else oi++;
     }
   if(g_nSu >= 40) { g_capRej++; return false; }
   ArrayResize(g_su, g_nSu + 1);
   g_su[g_nSu].model = model; g_su[g_nSu].dir = d; g_su[g_nSu].entry = entry; g_su[g_nSu].sl = sl; g_su[g_nSu].tp = tp;
   g_su[g_nSu].born = i; g_su[g_nSu].fillBar = filled ? i : 0; g_su[g_nSu].state = filled ? 1 : 0; g_su[g_nSu].score = sc;
   g_su[g_nSu].rr = rr; g_su[g_nSu].cost = InpCostAtr * g_atr[i]; g_su[g_nSu].finBar = -1; g_su[g_nSu].win = false;
   g_nSu++;
   g_evSig = d;
   return true;
  }

void FinishSetup(const int k, const bool win, const int i)
  {
   g_su[k].state = 2; g_su[k].finBar = i; g_su[k].win = win;
   if(win) g_win[g_su[k].model]++; else g_loss[g_su[k].model]++;
  }

void UpdSetups(const int i)
  {
   for(int k = g_nSu - 1; k >= 0; k--)
     {
      if(g_su[k].state == 0)
        {
         bool cancel = i - g_su[k].born > InpExpBars;
         if(g_su[k].dir == 1 && (g_c[i] < g_su[k].sl || g_h[i] >= g_su[k].tp)) cancel = true;
         if(g_su[k].dir == -1 && (g_c[i] > g_su[k].sl || g_l[i] <= g_su[k].tp)) cancel = true;
         if(cancel)
           {
            for(int j = k; j < g_nSu - 1; j++) g_su[j] = g_su[j + 1];
            g_nSu--;
           }
         else if(i > g_su[k].born && ((g_su[k].dir == 1 && g_l[i] <= g_su[k].entry) || (g_su[k].dir == -1 && g_h[i] >= g_su[k].entry)))
           {
            g_su[k].state = 1; g_su[k].fillBar = i;
            bool slNow = (g_su[k].dir == 1) ? g_l[i] <= g_su[k].sl + g_su[k].cost : g_h[i] >= g_su[k].sl - g_su[k].cost;
            if(slNow) FinishSetup(k, false, i);
           }
        }
      else if(g_su[k].state == 1 && i > g_su[k].fillBar)
        {
         bool slHit = (g_su[k].dir == 1) ? g_l[i] <= g_su[k].sl + g_su[k].cost : g_h[i] >= g_su[k].sl - g_su[k].cost;     // pessimistic: the stop is checked first
         bool tpHit = (g_su[k].dir == 1) ? g_h[i] >= g_su[k].tp + g_su[k].cost : g_l[i] <= g_su[k].tp - g_su[k].cost;
         if(slHit) FinishSetup(k, false, i);
         else if(tpHit) FinishSetup(k, true, i);
        }
     }
  }

double EntryPx(const double top, const double bot, const int d)
  {
   if(InpEntryMode == MD_E_EDGE) return d == 1 ? top : bot;
   return (top + bot) / 2.0;
  }

void ModelsBar(const int i)
  {
   double buf = InpSlBuf * g_atr[i];
   bool kz = KzAny(i);
   if(InpM22On)                                   // ICT 2022: raid -> MSS -> FVG
     {
      if(g_evMss != 0 && g_raidDir == g_evMss && i - g_raidBar <= InpMssWin && (g_t[i] - g_raidT) <= AgeSec() && (!InpKzOnly || kz))
        { g_w22Dir = g_evMss; g_w22Bar = i; g_w22Px = g_raidPx; }
      if(g_w22Dir != 0)
        {
         if(i - g_w22Bar > 3) g_w22Dir = 0;
         else
           {
            bool done = false;
            for(int k = g_nFvg - 1; k >= 0 && !done; k--)
              {
               if(g_fvg[k].dir == g_w22Dir && g_fvg[k].state == 0 && g_fvg[k].bar >= g_w22Bar)
                 {
                  double en = EntryPx(g_fvg[k].top, g_fvg[k].bot, g_fvg[k].dir);
                  double sl = g_w22Dir == 1 ? g_w22Px - buf : g_w22Px + buf;
                  if(NewSetup(1, g_w22Dir, en, sl, kz, false, i)) { g_w22Dir = 0; done = true; }      // the chain ends only when a setup was really made
                 }
              }
           }
        }
     }
   if(!SbAny(i)) g_sbFired = false;               // Silver Bullet
   if(InpMSbOn && SbAny(i) && !g_sbFired && g_evFvg != 0)
     {
      int d = g_nfDir;
      bool aligned = (g_mssDir == d && (g_t[i] - g_mssT) <= AgeSec()) || (g_raidDir == d && (g_t[i] - g_raidT) <= AgeSec()) || !InpSbBias;
      if(aligned)
        {
         double en = EntryPx(g_nfTop, g_nfBot, d);
         double sl = d == 1 ? g_nfExt - buf : g_nfExt + buf;
         if(NewSetup(2, d, en, sl, true, false, i)) g_sbFired = true;
        }
     }
   if(InpMUniOn && g_uniDir != 0 && !g_uniDone && i - g_uniBar <= 3 && (!InpKzOnly || kz))      // Unicorn: retried for 3 bars, ONE setup at most
     {
      int d = g_uniDir;
      double en = EntryPx(g_uniTop, g_uniBot, d);
      double sl = d == 1 ? g_uniFar - buf : g_uniFar + buf;
      if(NewSetup(3, d, en, sl, kz, false, i)) g_uniDone = true;
     }
   if(InpMOteOn && g_oteDir != 0 && !g_oteFired && (!InpKzOnly || kz))   // OTE: a limit order at the signal candle's close
     {
      bool origin = (InpOteSL == MD_O_ORIGIN);
      if(g_oteDir == 1 && g_l[i] <= g_oteTop && g_c[i] > g_oteBot && g_c[i] > g_oteMid && g_c[i] > g_o[i])
        {
         double slv = origin ? g_oteSl : MathMin(g_l[i], g_oteBot);
         if(NewSetup(4, 1, g_c[i], slv - buf, kz, false, i)) g_oteFired = true;
        }
      if(g_oteDir == -1 && g_h[i] >= g_oteBot && g_c[i] < g_oteTop && g_c[i] < g_oteMid && g_c[i] < g_o[i])
        {
         double slv = origin ? g_oteSl : MathMax(g_h[i], g_oteTop);
         if(NewSetup(4, -1, g_c[i], slv + buf, kz, false, i)) g_oteFired = true;
        }
     }
   UpdSetups(i);
  }

//+------------------------------------------------------------------+
//| one confirmed bar (same order as the Pine main block)            |
//+------------------------------------------------------------------+
void ProcessBar(const int i)
  {
   g_evRaid = 0; g_evRun = 0; g_evMss = 0; g_evBrk = 0; g_evFvg = 0; g_evUni = 0; g_evSig = 0;
   if(DispBull(i)) { g_dispBullBar = i; if(EDispMark) AddDisp(i, 1); }
   if(DispBear(i)) { g_dispBearBar = i; if(EDispMark) AddDisp(i, -1); }
   if(g_intra)
      for(int k = 0; k < 5; k++)
        {
         bool ended = SessUpd(k, Fl(i, k), i);
         if(ended) SessEnd(k, i);
        }
   PeriodsBar(i);
   FvgDetect(i);
   FvgUpdate(i);
   PivotsM(i);
   ChkLiq(i);
   Breaks(i);
   UpdZones(i);
   Gaps(i);
   Unicorn(i);
   ModelsBar(i);
  }

//+------------------------------------------------------------------+
//| drawing                                                          |
//+------------------------------------------------------------------+
color ZoneCol(const int kind, const int d)
  {
   if(kind == 2) return d == 1 ? C_CYAN : C_ORANGE;
   if(kind == 3) return d == 1 ? C_PURPLE : C_PINK;
   if(kind == 4) return C_YELLOW;
   if(kind == 5) return C_LIME;
   if(kind == 6) return C_GRAY;
   return d == 1 ? InpObBullC : InpObBearC;
  }

color LiqColor(const int kind, const string nm)
  {
   if(kind == 2) return InpPdC;
   if(kind == 3) return InpPwC;
   if(kind == 4) return InpPmC;
   if(kind == 5) return InpEqC;
   return InpSessLC;
  }

double g_near = 0.0;

void DrawFvgs()
  {
   datetime xe = TimeAt(g_n - 1 + InpLblPad - 1);
   for(int k = 0; k < g_nFvg; k++)
     {
      color c = g_fvg[k].dir == 1 ? (g_fvg[k].state == 1 ? C_CYAN : InpFvgBullC) : (g_fvg[k].state == 1 ? C_ORANGE : InpFvgBearC);
      datetime t1 = g_t[MathMax(0, g_fvg[k].bar - 2)];
      MkRect("FV", t1, g_fvg[k].top, xe, g_fvg[k].bot, Tint(c, 0.14), true);
      MkRect("FB", t1, g_fvg[k].top, xe, g_fvg[k].bot, Tint(c, 0.6), false);
      double mid = (g_fvg[k].top + g_fvg[k].bot) / 2.0;
      if(InpFvgCE) MkLine("FC", t1, mid, xe, mid, Tint(c, 0.7), STYLE_DOT, 1);
      if(MathAbs(mid - g_c[g_n - 1]) <= g_near) AddItem(g_fvg[k].nm, c, mid);
     }
  }

void DrawZones()
  {
   datetime xe = TimeAt(g_n - 1 + InpLblPad - 1);
   for(int k = 0; k < g_nZn; k++)
     {
      color c = ZoneCol(g_zn[k].kind, g_zn[k].dir);
      datetime t1 = g_t[MathMax(0, g_zn[k].bar)];
      MkRect("ZF", t1, g_zn[k].top, xe, g_zn[k].bot, Tint(c, g_zn[k].touched ? 0.08 : 0.15), true);
      MkRect("ZB", t1, g_zn[k].top, xe, g_zn[k].bot, Tint(c, 0.7), false);
      double mid = (g_zn[k].top + g_zn[k].bot) / 2.0;
      if(EZoneMt) MkLine("ZM", t1, mid, xe, mid, Tint(c, 0.6), STYLE_DOT, 1);
      if(MathAbs(mid - g_c[g_n - 1]) <= g_near) AddItem(g_zn[k].nm, c, mid);
     }
  }

void DrawMisc()
  {
   for(int k = 0; k < g_nMb; k++)
     {
      int b0 = MathMax(0, g_mb[k].b0);
      datetime t1 = g_t[b0], t2 = TimeAt(g_mb[k].b1);
      bool uni = (g_mb[k].txt == "Unicorn");
      MkRect("MB", t1, g_mb[k].top, t2, g_mb[k].bot, Tint(g_mb[k].col, uni ? 0.30 : 0.22), true);
      MkRect("MR", t1, g_mb[k].top, t2, g_mb[k].bot, uni ? clrWhite : Tint(g_mb[k].col, 0.9), false);
      MkText("MT", t1 + (datetime)((t2 - t1) / 2), (g_mb[k].top + g_mb[k].bot) / 2.0, g_mb[k].txt, uni ? clrWhite : g_mb[k].col, ANCHOR_CENTER);
     }
  }

void DrawStructure()
  {
   for(int k = 0; k < g_nSt; k++)
     {
      int f = Cl1400(g_st[k].fromBar);
      MkLine("SL", g_t[f], g_st[k].px, g_t[g_st[k].toBar], g_st[k].px, g_st[k].col, STYLE_DASH, 1);
      MkText("ST", g_t[(f + g_st[k].toBar) / 2], g_st[k].px, g_st[k].txt, g_st[k].col, ANCHOR_LOWER);
     }
  }

void DrawLiquidity()
  {
   datetime xe = TimeAt(g_n - 1 + InpLblPad - 1);
   for(int k = 0; k < g_nLq; k++)
     {
      color c = LiqColor(g_lq[k].kind, g_lq[k].name);
      datetime t0 = g_t[MathMax(g_lq[k].ob, MathMax(0, g_n - 1400))];
      if(g_lq[k].swept)
        {
         MkLine("LQ", t0, g_lq[k].price, g_t[g_lq[k].sweptBar], g_lq[k].price, c, STYLE_DOT, 1);
         MkText("LC", g_t[g_lq[k].sweptBar], g_lq[k].price, Ck(), c, ANCHOR_LEFT);
        }
      else
        {
         MkLine("LQ", t0, g_lq[k].price, xe, g_lq[k].price, c, STYLE_DASH, 1);
         if(MathAbs(g_lq[k].price - g_c[g_n - 1]) <= g_near * 1.5) AddItem(g_lq[k].name + " " + Fp(g_lq[k].price), c, g_lq[k].price);
        }
     }
  }

void DrawDealing()
  {
   if(!g_dealOk) return;
   datetime xe = TimeAt(g_n - 1 + InpLblPad - 1);
   datetime x0 = g_t[Cl1400(MathMin(g_swHiBar, g_swLoBar))];
   if(EPdZone)
     {
      MkRect("PD", x0, g_swHi, xe, g_eq, Tint(C_BEAR, 0.06), true);
      MkRect("PD", x0, g_eq, xe, g_swLo, Tint(C_BULL, 0.06), true);
      MkLine("PE", x0, g_eq, xe, g_eq, Tint(C_GRAY, 0.8), STYLE_DASH, 1);
      AddItem("Premium", C_BEAR, g_swHi);
      AddItem("EQ " + Fp(g_eq), C_GRAY, g_eq);
      AddItem("Discount", C_BULL, g_swLo);
     }
   if(InpOteOn)
     {
      color oc = g_oteDir == 1 ? C_BULL : C_BEAR;
      datetime xo = g_t[Cl1400(MathMax(g_swHiBar, g_swLoBar))];
      MkRect("OF", xo, g_oteTop, xe, g_oteBot, Tint(oc, 0.18), true);
      MkRect("OB", xo, g_oteTop, xe, g_oteBot, Tint(oc, 0.6), false);
      MkLine("OM", xo, g_oteMid, xe, g_oteMid, Tint(oc, 0.8), STYLE_DOT, 1);
      AddItem("OTE 70.5%", oc, g_oteMid);
     }
  }

void DrawMarks()
  {
   for(int k = 0; k < g_nMk; k++)
     {
      if(!g_mk[k].alive) continue;
      int cnt = 0;                                // several markers on the same bar are stacked
      for(int j = 0; j < k; j++) if(g_mk[j].alive && g_mk[j].bar == g_mk[k].bar) cnt++;
      double off = cnt * 0.7 * g_atr[MathMin(g_mk[k].bar, g_n - 1)];
      double y = g_mk[k].dir == 1 ? g_mk[k].price + off : g_mk[k].price - off;
      MkText("MK", g_t[g_mk[k].bar], y, g_mk[k].text, g_mk[k].col, g_mk[k].dir == 1 ? ANCHOR_LOWER : ANCHOR_UPPER);
     }
   for(int k = 0; k < g_nDp; k++)
      MkText("DP", g_t[g_dp[k].bar], (g_o[g_dp[k].bar] + g_c[g_dp[k].bar]) / 2.0, Bolt(), g_dp[k].dir == 1 ? C_BULL : C_BEAR, ANCHOR_CENTER);
  }

void DrawSetups()
  {
   if(!InpSigOn || !InpShowSetups) return;
   datetime xe = TimeAt(g_n - 1 + InpLblPad - 1);
   for(int k = 0; k < g_nSu; k++)
     {
      datetime t0 = g_t[g_su[k].born];
      datetime t1 = (g_su[k].state == 2) ? g_t[g_su[k].finBar] : xe;
      bool fin = (g_su[k].state == 2);
      MkLine("SE", t0, g_su[k].entry, t1, g_su[k].entry, fin ? Tint(C_GRAY, 0.8) : C_BLUE, STYLE_SOLID, 2);
      MkLine("SS", t0, g_su[k].sl, t1, g_su[k].sl, (fin && !g_su[k].win) ? Tint(C_GRAY, 0.3) : C_BEAR, STYLE_SOLID, 1);
      MkLine("SU", t0, g_su[k].tp, t1, g_su[k].tp, (fin && g_su[k].win) ? Tint(C_GRAY, 0.3) : C_BULL, STYLE_SOLID, 1);
      string arrow = g_su[k].dir == 1 ? " " + Up() + " " : " " + Dn() + " ";
      if(fin)
         MkText("SF", t1, g_su[k].entry, ModelName(g_su[k].model) + arrow + (g_su[k].win ? "TP " + Ck() : "SL x"), g_su[k].win ? C_BULL : C_BEAR, ANCHOR_LEFT);
      else
        {
         string lb = ModelName(g_su[k].model) + (g_su[k].dir == 1 ? " BUY " : " SELL ") + "*" + IntegerToString(g_su[k].score) + "/6  RR " + DoubleToString(g_su[k].rr, 1);
         if(g_su[k].state == 1) lb = ModelName(g_su[k].model) + arrow + "filled *" + IntegerToString(g_su[k].score);
         AddItem(lb, g_su[k].dir == 1 ? C_BULL : C_BEAR, g_su[k].entry);
        }
     }
  }

//+------------------------------------------------------------------+
//| panel                                                            |
//+------------------------------------------------------------------+
string DirTxt(const int d) { return d == 1 ? "Bullish " + Up() : (d == -1 ? "Bearish " + Dn() : "-"); }
color  DirCol(const int d) { return d == 1 ? C_BULL : (d == -1 ? C_BEAR : C_GRAY); }

string TfName(const ENUM_TIMEFRAMES tf)
  {
   int m = PeriodSeconds(tf) / 60;
   if(m < 60) return "M" + IntegerToString(m);
   if(m < 1440) return "H" + IntegerToString(m / 60);
   return "D" + IntegerToString(m / 1440);
  }

void UpdatePanel()
  {
   if(!InpShowPanel)
     {
      ObjectsDeleteAll(0, PFX + "PN");
      return;
     }
   if(g_n < 3) return;
   ChooseCorner();
   int L = g_n - 2;
   string actNm = "Outside killzones";
   color  actCol = C_GRAY;
   for(int k = 0; k < 5; k++) if(g_ms[k].on && g_ms[k].active) { actNm = g_ms[k].name; actCol = g_ms[k].col; }
   if(SbAny(L)) actNm += " + Silver Bullet";
   string rd = g_raidDir == 0 ? "-" : g_raidName + (g_raidRej ? " (raid)" : " (run)") + " - " + IntegerToString(L - g_raidBar) + " bars ago";
   string ms = g_mssDir == 0 ? "-" : DirTxt(g_mssDir) + " - " + IntegerToString(L - g_mssBar) + " bars ago";
   string pdTxt = "-";
   color  pdCol = C_GRAY;
   if(!IsNa(g_rngTop) && g_rngTop > g_rngBot)
     {
      double pos = (g_c[L] - g_rngBot) / (g_rngTop - g_rngBot) * 100.0;
      if(pos > 100.0) { pdTxt = "Above range (new highs)"; pdCol = C_BEAR; }
      else if(pos < 0.0) { pdTxt = "Below range (new lows)"; pdCol = C_BULL; }
      else { pdTxt = (pos >= 50.0 ? "Premium " : "Discount ") + IntegerToString((int)pos) + "%"; pdCol = pos >= 50.0 ? C_BEAR : C_BULL; }
     }
   string mode = "Analysis only (no signals)";
   color  modeCol = C_GRAY;
   if(InpSigOn)
     {
      int nOpen = 0;
      for(int k = 0; k < g_nSu; k++) if(g_su[k].state < 2) nOpen++;
      mode = "Setups: " + IntegerToString(nOpen) + " open";
      if(EStats)
        {
         mode = "";
         for(int m = 1; m <= 4; m++) mode += ModelName(m) + " " + IntegerToString(g_win[m]) + "W/" + IntegerToString(g_loss[m]) + "L  ";
        }
      if(g_capRej > 0) mode += " (rejected " + IntegerToString(g_capRej) + ": 40 open)";
      modeCol = C_ORANGE;
     }
   string bk = "GMT";
   if(InpBrokerGmt >= 0) bk += "+";
   bk += IntegerToString(InpBrokerGmt);
   if(InpBrokerDst != TM_DST_NONE) bk += " (+1 summer)";
   color  bkC = C_GRAY;
#ifdef __MQL5__
   datetime srv = TimeTradeServer(), gm = TimeGMT();
   if(srv > 0 && gm > 0)
     {
      int real = (int)MathRound((double)(srv - gm) / 1800.0);
      int mine = (int)MathRound((double)(srv - ServerToUtc(srv)) / 1800.0);
      if(real != mine) { bk += "  CHECK offset!"; bkC = C_BEAR; } else bkC = C_BULL;
     }
#endif
   PanelRow(0, "Crypex ICT Models", g_intra ? "" : "use <= H4", C_ORANGE);
   PanelRow(1, "Now", actNm, actCol);
   PanelRow(2, "HTF bias " + TfName(InpBiasTF) + "/" + IntegerToString(InpBiasLen), DirTxt(g_htf[L]), DirCol(g_htf[L]));
   PanelRow(3, "Last liquidity", rd, DirCol(g_raidDir));
   PanelRow(4, "Last MSS", ms, DirCol(g_mssDir));
   PanelRow(5, "Dealing range", pdTxt, pdCol);
   PanelRow(6, "Mode", mode, modeCol);
   PanelRow(7, "Broker clock", bk, bkC);
   PanelMeasure();
   int px = 0, py = 0;
   PanelOrigin(px, py);
   PanelBack(px, py);
   for(int r = 0; r < PN_ROWS; r++) PanelDrawRow(r, px, py);
  }

//+------------------------------------------------------------------+
//| alerts                                                           |
//+------------------------------------------------------------------+
void FireAlerts()
  {
   string names[10] = {"Buy-side liquidity raided (bearish expectation)", "Sell-side liquidity raided (bullish expectation)",
                       "Liquidity run (level broken and held, not a raid)", "Bullish MSS", "Bearish MSS", "New bullish FVG", "New bearish FVG",
                       "Unicorn zone formed", "BUY setup", "SELL setup"};
   for(int k = 0; k < 10; k++) if(g_alLast[k]) Notify(names[k]);
  }

void FlagAlerts()
  {
   for(int k = 0; k < 10; k++) g_alLast[k] = false;
   g_alLast[0] = (g_evRaid == -1); g_alLast[1] = (g_evRaid == 1); g_alLast[2] = (g_evRun != 0);
   g_alLast[3] = (g_evMss == 1);   g_alLast[4] = (g_evMss == -1);
   g_alLast[5] = (g_evFvg == 1);   g_alLast[6] = (g_evFvg == -1);
   g_alLast[7] = (g_evUni != 0);
   g_alLast[8] = (g_evSig == 1);   g_alLast[9] = (g_evSig == -1);
  }

//+------------------------------------------------------------------+
//| replay the whole window bar by bar, then draw the final state    |
//+------------------------------------------------------------------+
void Rebuild()
  {
   ObjectsDeleteAll(0, PFX);
   g_bg = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   g_intra = (PeriodSeconds(_Period) <= 14400);
   DetectMarket();
   InitModels();
   ResetState();
   BuildSeries();
   const int L = g_n - 2;                          // last CLOSED bar: no repainting
   for(int i = 0; i <= L; i++) ProcessBar(i);
   FlagAlerts();
   g_nItem = 0;
   g_near = Rng150() * ELblNear;
   DrawDealing();
   DrawFvgs();
   DrawZones();
   DrawMisc();
   DrawStructure();
   DrawLiquidity();
   DrawSetups();
   FlushItems();
   DrawMarks();
   UpdatePanel();
   PaintWatermark();
   if(g_firstDone && g_t[g_n - 1] != g_lastAlertBar) FireAlerts();
   g_lastAlertBar = g_t[g_n - 1];
   g_firstDone = true;
   ChartRedraw();
  }

//+------------------------------------------------------------------+
int OnInit()
  {
   IndicatorShortName("Crypex ICT Models");
   if(InpRightMargin > 0)
     {
      ChartSetInteger(0, CHART_SHIFT, true);
      ChartSetDouble(0, CHART_SHIFT_SIZE, (double)MathMin(InpRightMargin, 50));
     }
   return INIT_SUCCEEDED;
  }

void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, PFX);
  }

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_CHART_CHANGE && g_n >= 3) UpdatePanel();      // scroll / zoom / resize: re-pick the panel corner
  }

int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[],
                const double &open[], const double &high[], const double &low[], const double &close[],
                const long &tick_volume[], const long &volume[], const int &spread[])
  {
   int total = MathMin(rates_total, InpMaxBars);
   if(total < 300) return 0;
   bool newBar = (Time[0] != g_lastBar) || prev_calculated == 0;
   if(!newBar)
     {
      UpdatePanel();                               // the clock runs on every tick
      return rates_total;
     }
   g_lastBar = Time[0];
   g_n = total;
   ArrayResize(g_t, g_n); ArrayResize(g_o, g_n); ArrayResize(g_h, g_n); ArrayResize(g_l, g_n); ArrayResize(g_c, g_n);
   for(int i = 0; i < g_n; i++)
     {
      int s = g_n - 1 - i;              // MT4 series arrays: index 0 = newest bar
      g_t[i] = Time[s];
      g_o[i] = Open[s];
      g_h[i] = High[s];
      g_l[i] = Low[s];
      g_c[i] = Close[s];
     }
   Rebuild();
   return rates_total;
  }
//+------------------------------------------------------------------+

منطق کار

  • ساختار: سقف و کف‌های سوئینگ با پیوت تأییدشده پیدا می‌شوند (طول سوئینگ قابل تنظیم). شکست آخرین سقف یا کف با کلوز کندل ثبت می‌شود. شکست خلاف روند قبلی «MSS» است و شکست در جهت روند «BOS». اگر کندل شکست یک دیسپلیسمنت باشد (بدنه ≥ ۱٫۲×ATR و ≥ ۶۰٪ دامنه) کنار MSS علامت ⚡ می‌آید.
  • Fair Value Gap: شکاف سه‌کندلی (low کندل سوم بالاتر از high کندل اول و برعکس) با حداقل اندازه‌ی نسبت به ATR. در Preset «Clean» فیلتر کیفیت روشن است: کندل وسط باید هم‌جهت شکاف باشد، بدنه‌اش حداقل ۰٫۵×ATR باشد و فراتر از سقف یا کف کندل اول ببندد. خط CE (۵۰٪ شکاف) نمایش داده می‌شود و شکاف با پر شدن جزئی کوچک می‌شود. اگر کندلی از لبه‌ی دور شکاف عبور کند و آن‌طرف ببندد، شکاف به IFVG تبدیل می‌شود.
  • Order Block، Breaker و Mitigation: بعد از BOS، آخرین کندل مخالف قبل از حرکت به‌عنوان OB علامت می‌خورد. بعد از MSS، اگر سوئینگ قبلی با Lower Low یا Higher High جارو شده باشد ناحیه «Breaker» و در غیر این‌صورت «Mitigation» است. این دو تشخیص تقریبی‌اند (در تنظیمات هم با «approx.» نوشته شده‌اند) و تأییدشده‌ی کامل ICT نیستند. ناحیه با کلوز فراتر از لبه‌ی دورش باطل می‌شود.
  • Unicorn: هم‌پوشانی یک Breaker ساخته‌شده توسط همین MSS با یک FVG سالم که در همان حرکت (از ۳ کندل قبل تا N کندل بعد از MSS) ساخته شده است.
  • پریمیوم و دیسکانت: آخرین رنج سوئینگ به دو نیم تقسیم می‌شود؛ بالای ۵۰٪ پریمیوم و پایین آن دیسکانت. ناحیه‌ی OTE (۶۲٪ تا ۷۹٪ با خط ۷۰٫۵٪) روی آخرین پای حرکت رسم می‌شود. اگر قیمت بیرون رنج باشد، پنل «Above range» یا «Below range» می‌نویسد.
  • نقدینگی: PDH/PDL، PWH/PWL، EQH/EQL و اختیاری سقف و کف سشن‌ها و PMH/PML. Raid یعنی سطح رد شد و کندل برگشت؛ اگر بیرون ببندد «Break?» است و ظرف چند کندل تکلیف می‌شود: برگشت = Raid و ماندن = Run. در یک کندل با چند سطح، سطح مهم‌تر انتخاب می‌شود.
  • Presetها: «Clean» (پیش‌فرض) فقط ارزشمندترین اجزا را نشان می‌دهد: MSS، FVG سالم و محدود، OB، Breaker، Unicorn، OTE و نقدینگی روز و هفته. «Standard» همه‌ی اجزا، «Full» بیشترین تعداد و «Custom» فقط تنظیمات دستی شماست.
  • پیش‌فرض تحلیلی است: مدل‌های ICT 2022 (Raid ← MSS ← FVG)، Silver Bullet، Unicorn و OTE فقط وقتی «Trade setups» را روشن کنید ستاپ با Entry، SL و TP می‌سازند. شبیه‌سازی آمار برد و باخت ساده است و بک‌تست نیست.
  • برچسب‌ها: برچسب‌های سمت راست بر اساس قیمت مرتب می‌شوند و روی هم نمی‌افتند؛ بلوک‌ها و FVGهای دور از قیمت برچسب ندارند ولی ناحیه‌شان می‌ماند.
  • بدون ریپینت: همه‌ی رویدادها روی کندل بسته‌شده ثبت می‌شوند. بایاس تایم‌فریم بالاتر فقط از آخرین کندل بسته‌ی آن تایم‌فریم گرفته می‌شود.
  • ورژن متاتریدر: همان قواعد و همان ترتیب پردازش (ساختار، FVG، بلوک‌ها، Unicorn، مدل‌ها) را دارد. ساعت سرور بروکر به UTC و بعد به نیویورک و تهران تبدیل می‌شود (قانون تغییر ساعت آمریکا و اروپا داخل کد است). هر بار کندل جدیدی بسته شود، پنجره‌ی محاسبه از اول بازپخش می‌شود و فقط حالت نهایی رسم می‌شود. بایاس تایم‌فریم بالاتر از کندل‌های بسته‌ی همان تایم‌فریم محاسبه می‌شود. اندازه‌ی پنل و واترمارک از روی عرض واقعی متن تنظیم می‌شود و پنل در حالت Auto خلوت‌ترین گوشه‌ی چارت را انتخاب می‌کند.

تنظیمات مهم

Preset
Clean، Standard، Full یا Custom. در سه حالت اول، تنظیمات «جزئیات نمایش» (BOS، IFVG، VI، BPR، Mitigation، Rejection، Propulsion، Vacuum، پریمیوم/دیسکانت، لول‌های سشن و سقف تعداد FVG/بلوک/لول) از Preset می‌آید. در Custom همان گزینه‌های پایین اعمال می‌شود.
Show times on chart in
منطقه‌ی زمانی ساعت‌ها؛ پیش‌فرض تهران.
Right-side labels: gap / offset / show within
فاصله‌ی برچسب‌های سمت راست، فاصله از آخرین کندل و حد فاصله‌ی قیمتی برای نمایش برچسب بلوک‌ها. حاشیه‌ی راست چارت را حدود ۱۵ تا ۲۰ باز کنید.
Swing length
طول پیوت سوئینگ برای ساختار؛ بزرگ‌تر = ساختار کلی‌تر و کندتر.
Displacement ATR / body ratio
شرط دیسپلیسمنت: بدنه ≥ ضریب×ATR و نسبت بدنه به دامنه.
Min FVG size / Quality filter / middle candle body
حداقل اندازه‌ی FVG و فیلتر کیفیت کندل وسط. در Clean، فیلتر همیشه روشن است.
Max active FVGs / Max blocks / Max liquidity levels
سقف تعداد FVG، بلوک و لول روی چارت (در Preset تعیین می‌شود).
Unicorn: FVG within N bars after the MSS
پنجره‌ی ساخته‌شدن FVG بعد از MSS برای Unicorn.
Trade setups (Entry / SL / TP)
پیش‌فرض خاموش. با روشن‌کردن، مدل‌های ICT 2022، Silver Bullet، Unicorn و OTE ستاپ می‌سازند.
Min confluence score / Min RR / Stop buffer / Trading cost
فقط وقتی ستاپ روشن است: حداقل امتیاز (از ۶)، حداقل ریسک به ریوارد، فاصله‌ی حد ضرر و هزینه‌ی تقریبی معامله.
Event validity (minutes)
مدت اعتبار Raid و MSS برای امتیاز و مدل‌ها؛ بر حسب دقیقه‌ی واقعی.
HTF bias timeframe / EMA length
بایاس تایم‌فریم بالاتر از EMA؛ فقط یک امتیاز از ۶ است مگر «Require HTF bias» روشن شود.
Broker server time (GMT offset in WINTER) و Broker daylight-saving rule (فقط متاتریدر)
اختلاف ساعت سرور بروکر با GMT در زمستان و قانون ساعت تابستانی بروکر (US، EU یا None). بیشتر بروکرهای فارکس GMT+2 در زمستان و GMT+3 در تابستان هستند.
Panel position / Watermark (متاتریدر)
جای پنل: Auto (خلوت‌ترین گوشه‌ی چارت) یا یکی از ۴ گوشه. واترمارک پایین‌چپ کنار لوگوی چارت است و فاصله‌اش قابل تنظیم است.
Panel position / Watermark position (TradingView)
جای جدول و واترمارک؛ هر کدام از ۹ نقطه‌ی چارت.

کجا بهتر کار می‌کند

تحلیل ساختار و نواحی ICT روی طلا، فارکس و شاخص‌ها در تایم‌فریم ۱ تا ۱۵ دقیقه (حداکثر ۴ ساعته): دیدن MSS، FVGهای سالم، بلوک‌ها، پریمیوم و دیسکانت و Raid نقدینگی در کنار هم، برای ساختن نقشه‌ی روز و تصمیم دستی. همراه «Crypex ICT Time» که سشن‌ها و نقدینگی کامل را رسم می‌کند، کامل‌تر است.

نقطه‌ضعف

در حالت پیش‌فرض سیگنال ورود و خروج نمی‌دهد و توصیه‌ی معاملاتی نیست. Breaker و Mitigation تشخیص تقریبی‌اند و OB فقط کندل مخالف قبل از شکست است، نه اعتبارسنجی کامل ICT. تعریف FVG، IFVG و Unicorn برداشت رایج جامعه‌ی ICT است و منبع رسمی ندارد؛ حداقل اندازه و ضریب بدنه‌ی کندل وسط حدسی‌اند و باید روی نماد خودتان تنظیم شوند. اگر ستاپ را روشن کنید، آمار برد و باخت یک شبیه‌سازی ساده روی کندل‌های بسته است (ترتیب حرکت داخل کندل معلوم نیست و سیاست محافظه‌کارانه دارد) و بک‌تست یا پیش‌بینی نیست؛ این اندیکاتور روی داده‌ی تاریخی بک‌تست نشده است. SMT در این اندیکاتور وجود ندارد. روی تایم‌فریم بالای ۴ ساعته کار نمی‌کند. کد TradingView کامپایل و روی چارت دیده شده است. کد MT4 و MT5 در MetaEditor کامپایل نشده؛ فقط با شبیه‌ساز و داده‌ی واقعی طلا اجرا شده است (بدون خطای اجرا، و خروجی MT4 و MT5 با هم یکی است) ولی با نسخه‌ی TradingView مقایسه‌ی عددی نشده، چون اینجا امکان اجرای Pine نیست. قبل از استفاده آن را کامپایل کنید و روی حساب دمو ببینید. در متاتریدر ساعت سشن‌ها از ساعت سرور بروکر محاسبه می‌شود، پس ساعت و قانون تغییر ساعت بروکر را باید درست وارد کنید؛ کنترل خودکار ساعت بروکر در پنل فقط در MT5 هست. برچسب‌ها در متاتریدر یک‌خطی‌اند و شفافیت ندارند؛ رنگ نواحی با پس‌زمینه‌ی چارت ترکیب می‌شود. تصاویر پیش‌نمایش بالا اسکرین‌شات TradingView نیستند؛ خروجی کد نسخه‌ی MQL روی کندل‌های واقعی طلا هستند که با یک رندرکننده‌ی ساده کشیده شده‌اند، پس ظاهر دقیق (فونت، شفافیت، برچسب‌ها) در TradingView و متاتریدر فرق می‌کند.

راهنمای کامل استفاده

معنی رنگ‌ها و علامت‌ها

  • FVG: سبز = صعودی، قرمز = نزولی. فیروزه‌ای/نارنجی = IFVG (شکاف معکوس‌شده). خط نقطه‌چین وسط = CE.
  • بلوک‌ها: سبز/قرمز = Order Block، فیروزه‌ای/نارنجی = Breaker، بنفش/صورتی = Mitigation، زرد = Rejection، لیمویی = Propulsion، خاکستری = Gap.
  • خط‌چین رنگی با MSS/BOS = شکست ساختار. ⚡ = دیسپلیسمنت.
  • «Raid X» نارنجی = سطح جارو شد و کندل برگشت. «Break? X» خاکستری = شکست و در انتظار تکلیف. «Run X» = شکست ماند.
  • قاب سفید با «Unicorn» = هم‌پوشانی Breaker و FVG.
  • قرمز کم‌رنگ بالا = پریمیوم، سبز کم‌رنگ پایین = دیسکانت، خط خاکستری = EQ (۵۰٪).

روش پیشنهادی (تحلیلی)

  • جهت بزرگ‌تر را از بایاس HTF و محل قیمت در رنج (پریمیوم یا دیسکانت) ببینید.
  • دنبال Raid نقدینگی بگردید، بعد MSS هم‌جهت برگشت و بعد FVG یا Breaker که در حرکت MSS ساخته شده.
  • ناحیه‌های سالم نزدیک قیمت را به‌عنوان مناطق احتمالی واکنش علامت بزنید؛ ورود و مدیریت ریسک با خودتان است.
  • اگر ستاپ‌ها را روشن کنید، آن‌ها را ابزار کمکی ببینید، نه سیگنال.

متاتریدر: تنظیم ساعت بروکر

  • ساعت سشن‌های ICT به وقت نیویورک است ولی متاتریدر ساعت سرور بروکر را می‌دهد. اندیکاتور ساعت سرور را به UTC و بعد به نیویورک و تهران تبدیل می‌کند، پس ساعت بروکر باید درست وارد شود.
  • ورودی «Broker server time (GMT offset in WINTER)»: اختلاف ساعت سرور با GMT در زمستان؛ بیشتر بروکرها عدد ۲ هستند.
  • ورودی «Broker daylight-saving rule»: US برای بروکرهای رایج «بسته‌شدن نیویورک» (GMT+2 زمستان، GMT+3 تابستان)، EU برای بروکرهای اروپایی و None اگر بروکر ساعت تابستانی ندارد.
  • راه کنترل: ساعت Market Watch را با ساعت UTC مقایسه کنید. در MT5 ردیف «Broker clock» پنل سبز می‌شود و اگر تنظیم غلط باشد قرمز با پیام «CHECK offset!».
  • بایاس تایم‌فریم بالاتر (HTF) در متاتریدر به داده‌ی همان تایم‌فریم نیاز دارد؛ اگر بروکر داده‌ی کافی نداده باشد، یک بار چارت H4 همان نماد را باز و اسکرول کنید تا بارگذاری شود.

نصب و تنظیم اولیه

  • کد را در Pine Editor بچسبانید و Add to chart بزنید.
  • روی Preset «Clean» بمانید تا چارت خلوت باشد؛ برای دیدن بیشتر Standard را انتخاب کنید.
  • در Settings > Scales حاشیه‌ی راست را حدود ۱۵ تا ۲۰ باز کنید تا برچسب‌ها دیده شوند.
  • جای پنل و واترمارک را از تنظیمات «Panel & watermark» عوض کنید.
  • متاتریدر: فایل را در MetaEditor با F7 کامپایل کنید و از Navigator روی چارت بکشید. جای پنل در حالت Auto خودش خلوت‌ترین گوشه را انتخاب می‌کند.

پرسش‌های متداول

این اندیکاتور سیگنال خرید و فروش می‌دهد؟

به‌طور پیش‌فرض نه. فقط تحلیلی است. مدل‌های ورود را باید خودتان با گزینه‌ی «Trade setups» روشن کنید و حتی آن‌وقت نتیجه توصیه‌ی معاملاتی نیست.

چرا FVGهای کمی می‌بینم؟

در Preset «Clean» فقط FVGهایی که کندل وسط‌شان یک حرکت قوی و هم‌جهت است و اندازه‌ی کافی دارند ثبت می‌شوند. برای دیدن همه‌ی شکاف‌ها Preset را Standard یا Full کنید.

فرق MSS و BOS چیست؟

MSS شکست ساختار خلاف روند قبلی است (احتمال تغییر جهت) و BOS شکست در جهت روند است. هر دو با کلوز کندل ثبت می‌شوند.

IFVG یعنی چه؟

وقتی کندلی از لبه‌ی دور یک FVG عبور کند و آن‌طرف ببندد، شکاف دیگر در جهت اولش کار نمی‌کند و نقش مخالف می‌گیرد؛ به آن IFVG می‌گوییم. این قاعده برداشت رایج است.

چرا بعضی برچسب‌ها ناپدید می‌شوند؟

برچسب بلوک‌ها، FVGها و لول‌های دور از قیمت مخفی می‌شود تا ستون راست شلوغ نشود. خود ناحیه روی چارت می‌ماند. حد را با «Show zone labels within» عوض کنید.

نسخه‌ی متاتریدر دارد؟

بله، MT5 و MT4 با همان قواعد. این دو نسخه را در MetaEditor کامپایل نکرده‌ایم و فقط با شبیه‌ساز و داده‌ی واقعی آزمایش شده‌اند؛ قبل از استفاده کامپایل کنید و روی دمو ببینید. ساعت بروکر را هم باید درست تنظیم کنید.

چرا در متاتریدر ساعت سشن‌ها غلط است؟

معمولاً ساعت سرور بروکر یا قانون ساعت تابستانی در تنظیمات اندیکاتور درست وارد نشده است. اختلاف ساعت سرور با GMT را در زمستان وارد کنید و قانون US یا EU یا None را مطابق بروکر خود بگذارید.

اندیکاتور همراه: همراه این اندیکاتور: Crypex ICT Time (سشن‌ها، CBDR، Judas و نقدینگی کامل با وقت تهران)

این کد آموزشی است و توصیه‌ی سرمایه‌گذاری نیست. قبل از استفاده روی حساب واقعی، کامپایل و رفتار آن را روی حساب دمو و در بک‌تست بررسی کنید؛ نتیجه‌ی گذشته تضمین‌کننده‌ی آینده نیست.

← همه‌ی اسکریپت‌ها