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

پروفایل حجمی · اندیکاتور

پروفایل حجمی Crypex: POC، Value Area، HVN/LVN و Naked POC

پروفایل حجمی کامل برای TradingView، متاتریدر ۵ و متاتریدر ۴: POC و Value Area با سه روش محاسبه، گره‌های HVN/LVN، Naked POC، خطوط در حال شکل‌گیری POC/VA، Initial Balance، پروفایل ترکیبی و لنگرشده، چند پروفایل سشن هم‌زمان (آسیا/لندن/نیویورک/دلخواه)، نواحی هم‌پوشانی سطوح و جدول وضعیت کامل با هشدار.

پیش‌نمایش

اندیکاتور پروفایل حجمی Crypex روی بیت‌کوین با POC، Value Area و گره‌های HVN/LVN
بیت‌کوین ۱۵ دقیقه‌ای: سه پروفایل روزانه‌ی پشت‌سرهم با POC، VAH/VAL، گره‌های HVN/LVN و جدول وضعیت کامل (نوع حجم، پوشش ریزکندل، Naked POC و غیره).رندر از خروجی کدبیت‌کوین (بایننس BTCUSDT)، ۱۵ دقیقه، تا ۲ نوامبر ۲۰۲۶
اندیکاتور پروفایل حجمی Crypex با خطوط در حال شکل‌گیری POC و Value Area
همان داده با خطوط Developing POC/VA (مسیر تغییر POC و Value Area کندل‌به‌کندل در طول دوره‌ی جاری) روی سه پروفایل روزانه.رندر از خروجی کدبیت‌کوین (بایننس BTCUSDT)، ۳۰ دقیقه، تا ۲ نوامبر ۲۰۲۶

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

volume-profile-crypex.pine · Pine Script v6 · ۱٬۸۶۸ خط
// Crypex Volume Profile — POC / Value Area / HVN-LVN / naked POC, with intrabar volume. Educational tool, not investment advice.
// © Crypex.org — All rights reserved. | حق نشر محفوظ برای Crypex.org
// License: personal, non-commercial use only. Redistribution or resale without written permission is prohibited.
// مجوز: استفاده شخصی و غیرتجاری. بازنشر یا فروش مجدد بدون اجازه‌ی کتبی ممنوع است.
//@version=6
// Crypex Volume Profile
//  PROFILES  Day / Week / Month / custom Session (one profile per period) or the last N bars (rolling).
//  VOLUME    Each lower-timeframe (intrabar) candle is spread uniformly over its High-Low range and split between the price rows it touches.
//            Up/Down volume = close >= open / close < open (TradingView convention). When intrabar data is missing the chart bar is used instead.
//            Volume type comes from the symbol: crypto = exchange volume (base/quote), forex / CFD = TICK volume (an approximation, not real turnover).
//  LEVELS    POC (highest-volume row), Value Area (default 70 %) with VAH / VAL, developing POC / VA, HVN / LVN nodes, naked (untouched) POCs.
//  VA RULE   Start at the POC and add rows outward. Three selectable methods:
//            TradingView (documented): compare the next row above / below, take the larger (tie -> closer to POC, then the upper one);
//                                      if that row would EXCEED the remaining target volume the expansion stops (coverage can be below the target).
//            Target-reached 1 row:     same walk but a row is always added until the target is reached or passed.
//            Pair 2 rows (CME/Dalton): compare the sums of the next two rows above / below and add the larger pair until the target is reached.
// Profile state updates on CONFIRMED bars only: the live bar joins the profile when it closes and finished profiles are not recalculated.
// (Historical results can still change if the data feed corrects history or the intrabar history window moves.)
indicator(title = 'Crypex Volume Profile', shorttitle = 'Crypex VP', overlay = true, max_bars_back = 5000, max_boxes_count = 500, max_lines_count = 500, max_labels_count = 500)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// CONSTANTS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BRAND_GREEN = #0BA31C
BRAND_DARK  = #1E2229
C_UP        = #089981
C_DN        = #F23645
C_BLUE      = #2962FF
C_ORANGE    = #FF9800
C_CYAN      = #00BCD4
C_PINK      = #E91E63
C_GRAY      = #787B86

M_AUTO  = 'Auto'
M_DAY   = 'Day'
M_WEEK  = 'Week'
M_MONTH = 'Month'
M_SESS  = 'Session'
M_ROLL  = 'Last N bars'

G1 = '1 · Profile'
G2 = '2 · Histogram'
G3 = '3 · Levels (POC / VAH / VAL)'
G4 = '4 · Developing POC / VA'
G5 = '5 · Nodes (HVN / LVN)'
G6 = '6 · Naked POC'
G7 = '7 · Status table'
G8 = '8 · Watermark'
G9  = '9 · Analytics (table)'
G10 = '10 · Initial Balance'
G11 = '11 · Confluence zones'
G12 = '12 · Composite profile'
G13 = '13 · Anchored profile'
G14 = '14 · Multi-session profiles'

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// INPUTS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
mode      = input.string(M_AUTO, 'Profile period', options = [M_AUTO, M_DAY, M_WEEK, M_MONTH, M_SESS, M_ROLL], group = G1, tooltip = 'Auto (پیش‌فرض): بر اساس تایم‌فریم چارت خودکار انتخاب می‌شود (درون‌روزی زیر ۱ روز → Day، دیلی → Week، هفتگی → Month، ماهانه به بالا → Last N bars) تا روی هیچ تایم‌فریمی خالی نماند. Day / Week / Month: برای هر دوره یک پروفایل ساخته می‌شود (مرز دوره طبق منطقه‌ی زمانی صرافی). اگر دستی انتخاب شوند، روی چارت دیلی/هفتگی/ماهانه یا بالاتر قابل استفاده نیستند چون کندل ریزتری داخل دوره وجود ندارد (پیام "Chart TF ≥ period" در جدول دیده می‌شود). Session: فقط ساعت‌های دلخواه شما (مثلاً لندن). Last N bars: یک پروفایل متحرک روی N کندل آخر، روی هر تایم‌فریمی کار می‌کند و وابسته به ریزکندل نیست.')
sessStr   = input.session('0800-1600', 'Session hours', group = G1, tooltip = 'فقط برای حالت Session.')
sessTz    = input.string('Europe/London', 'Session time zone', group = G1, tooltip = 'فقط برای حالت Session. مثال: Europe/London، America/New_York، Asia/Tehran، UTC.')
rollN     = input.int(300, 'N (Last N bars mode)', minval = 20, maxval = 1500, group = G1)
rollLive  = input.bool(false, 'Last N bars: include the live (forming) bar', group = G1, tooltip = 'خاموش (پیش‌فرض): فقط کندل‌های بسته‌شده، مثل نسخه‌های MT4/MT5؛ POC و Value Area با کندل باز تغییر نمی‌کنند. روشن: کندل در حال تشکیل هم محاسبه می‌شود و تا بسته‌شدن آن کندل خروجی ممکن است تغییر کند (repaint).')
keepPrev  = input.int(2, 'Previous profiles to show', minval = 0, maxval = 10, group = G1, tooltip = 'تعداد پروفایل‌های قبلی که روی چارت نمایش داده می‌شوند. سقف تعداد باکس‌های TradingView ۵۰۰ است؛ اگر ردیف‌ها زیاد باشند پروفایل‌های قدیمی‌تر فقط به‌صورت خطوط نمایش داده می‌شوند.')
rows      = input.int(70, 'Rows per profile', minval = 10, maxval = 200, group = G1, tooltip = 'تعداد ردیف‌های قیمتی در هر پروفایل (با اندازه‌ی ردیف Auto). ردیف بیشتر = جزئیات بیشتر.')
rowMode   = input.string('Auto', 'Row size', options = ['Auto', 'Fixed ticks'], group = G1, tooltip = 'Auto: اندازه‌ی ردیف از میانگین دامنه‌ی ۵ پروفایل قبلی تقسیم بر تعداد ردیف‌ها به‌دست می‌آید (برای پروفایل اول از ATR تخمین زده می‌شود). Fixed ticks: اندازه‌ی ردیف ثابت. (حالت «تعداد ردیف دقیق» برای پروفایل جاری در Pine ممکن نیست چون ردیف‌ها کندل‌به‌کندل ساخته می‌شوند و دامنه‌ی نهایی از قبل معلوم نیست؛ در MT4/MT5 موجود است.) در حالت Last N bars همیشه دامنه‌ی N کندل تقسیم بر تعداد ردیف است.')
rowTicks  = input.int(10, 'Fixed row size (ticks)', minval = 1, maxval = 100000, group = G1)
vaPct     = input.float(70.0, 'Value Area %', minval = 30, maxval = 95, step = 1, group = G1)
vaMethod  = input.string('TradingView (documented)', 'Value Area method', options = ['TradingView (documented)', 'Target-reached · 1 row', 'Pair · 2 rows (CME / Dalton)'], group = G1, tooltip = 'TradingView (documented): هر بار یک ردیف بالا و پایین مقایسه می‌شود؛ اگر ردیف انتخابی از حجم باقی‌مانده‌ی هدف بیشتر باشد توقف می‌کند (پوشش ممکن است کمی کمتر از ۷۰٪ بماند). Target-reached: ردیف‌ها همیشه تا رسیدن یا عبور از هدف اضافه می‌شوند. Pair: جفت‌ردیف‌های بالا و پایین مقایسه و جفت بزرگ‌تر اضافه می‌شود. جدول وضعیت درصد واقعی پوشش را نشان می‌دهد.')
volSrc    = input.string('Fallback to chart bar', 'Volume source', options = ['Fallback to chart bar', 'Strict intrabars', 'Chart bars only'], group = G1, tooltip = 'Fallback to chart bar: حجم ریزکندل‌ها استفاده می‌شود، ولی اگر جمع حجم ریزکندل‌های یک کندل با حجم خود کندل بیش از «Intrabar volume tolerance» فرق داشت (داده‌ی ناقص)، همان کندل چارت استفاده می‌شود؛ هیچ‌وقت هر دو با هم جمع نمی‌شوند. Strict intrabars: فقط ریزکندل‌ها، هر چه موجود باشد (کندل بدون ریزکندل در پروفایل وارد نمی‌شود). Chart bars only: حجم هر کندل چارت روی دامنه‌ی خودش پخش می‌شود. تاریخچه‌ی ریزکندل در TradingView محدود است.')
volTol    = input.float(2.0, 'Intrabar volume tolerance (%)', minval = 0.0, maxval = 50.0, step = 0.5, group = G1, tooltip = 'حداکثر اختلاف مجاز بین جمع حجم ریزکندل‌ها و حجم کندل چارت (فقط برای Fallback). حجم تیک و حجم صرافی باید با هم جمع شوند؛ اختلاف بیشتر یعنی ریزکندل ناقص است.')
sessBound = input.string('Exclude incomplete boundary', 'Session boundary bar without intrabars', options = ['Exclude incomplete boundary', 'Estimated partial boundary'], group = G1, tooltip = 'فقط برای حالت Session وقتی ریزکندل در دسترس نیست: کندل مرزی که فقط بخشی از آن داخل سشن است. Exclude (پیش‌فرض و امن) = کل کندل کنار گذاشته می‌شود و پروفایل به‌عنوان PARTIAL علامت می‌خورد؛ Estimated = حجم به نسبت زمان داخل سشن تقسیم و افزوده می‌شود (تخمین، علامت‌گذاری می‌شود). برای دقت کامل از Strict intrabars استفاده کنید.')
ltfIn     = input.string('Auto', 'Intrabar timeframe', options = ['Auto', '1', '3', '5', '15', '30', '60', '240'], group = G1, tooltip = 'Auto: کوچک‌ترین تایم‌فریمی که هر کندل چارت را حداکثر به ۱۰۰ ریزکندل تقسیم کند.')

showHist  = input.bool(true, 'Show histogram', group = G2)
histSide  = input.string('Left → right', 'Direction', options = ['Left → right', 'Right → left'], group = G2, tooltip = 'Left → right (پیش‌فرض): هیستوگرام از ابتدای دوره به سمت راست رشد می‌کند. Right → left: از انتهای دوره/لحظه‌ی فعلی به سمت چپ رشد می‌کند و روی کندل‌های اخیر می‌افتد.')
widthPct  = input.int(35, 'Width (% of period length)', minval = 5, maxval = 100, group = G2)
splitUD   = input.bool(true, 'Split Up / Down volume', group = G2, tooltip = 'روشن: هر ردیف دو رنگ دارد (حجم کندل‌های صعودی/نزولی). خاموش: یک رنگ برای داخل Value Area و یک رنگ برای بیرون آن. روشن‌بودن دو برابر باکس مصرف می‌کند.')
upCol     = input.color(C_UP, 'Up volume', group = G2, inline = 'c1')
dnCol     = input.color(C_DN, '', group = G2, inline = 'c1')
vaCol     = input.color(C_BLUE, 'Inside VA (single colour)', group = G2, inline = 'c2')
outCol    = input.color(C_GRAY, '', group = G2, inline = 'c2')
pocRowCol = input.color(C_ORANGE, 'POC row', group = G2)
inTr      = input.int(35, 'Inside-VA transparency', minval = 0, maxval = 95, group = G2)
outTr     = input.int(70, 'Outside-VA transparency', minval = 0, maxval = 95, group = G2)

showPoc   = input.bool(true, 'POC line', group = G3, inline = 'l1')
pocCol    = input.color(C_ORANGE, '', group = G3, inline = 'l1')
pocW      = input.int(2, '', minval = 1, maxval = 5, group = G3, inline = 'l1')
showVA    = input.bool(true, 'VAH / VAL lines', group = G3, inline = 'l2')
vaLineCol = input.color(C_BLUE, '', group = G3, inline = 'l2')
vaLineW   = input.int(1, '', minval = 1, maxval = 5, group = G3, inline = 'l2')
shadeVA   = input.bool(true, 'Shade Value Area', group = G3)
extPrev   = input.bool(true, 'Extend the latest finished profile into the current period', group = G3, tooltip = 'POC / VAH / VAL دوره‌ی قبل (مثل «دیروز») تا انتهای چارت ادامه پیدا می‌کنند؛ اغلب به‌عنوان حمایت و مقاومت استفاده می‌شوند.')
lineExt   = input.int(10, 'Extend lines into the future (bars)', minval = 0, maxval = 100, group = G3)
showLbl   = input.bool(true, 'Level labels (price)', group = G3, inline = 'lb')
lblSizeStr = input.string('small', '', options = ['tiny', 'small', 'normal', 'large'], group = G3, inline = 'lb')

devOn     = input.bool(false, 'Developing POC / VA lines', group = G4, tooltip = 'مسیر تغییر POC و Value Area در طول دوره‌ی جاری، کندل‌به‌کندل (فقط روی کندل‌های بسته‌شده).')
devPocCol = input.color(C_ORANGE, 'Developing POC', group = G4, inline = 'd1')
devVaCol  = input.color(C_BLUE, '', group = G4, inline = 'd1')
devFill   = input.bool(true, 'Fill between VAH and VAL', group = G4)

nodesOn   = input.bool(true, 'Mark HVN / LVN', group = G5, tooltip = 'HVN = قله‌ی حجم (ناحیه‌ی تعادل). LVN = دره‌ی حجم بین دو قله (ناحیه‌ی عبور سریع). فقط برای پروفایل جاری و آخرین پروفایل‌های قبلی علامت می‌خورد.')
nodeMode  = input.string('Advanced', 'Node detection', options = ['Advanced', 'Simple'], group = G5, tooltip = 'Advanced: قله‌های برجسته (Prominence) با حداقل فاصله و ناحیه‌ی هر گره. Simple: روش قدیمی قله‌ی محلی در یک پنجره.')
nodeProfs = input.int(2, 'Profiles with nodes (newest)', minval = 1, maxval = 6, group = G5)
nodeSmooth = input.float(2.0, 'Advanced: smoothing half-width (% of rows)', minval = 0.5, maxval = 10, step = 0.5, group = G5, tooltip = 'نیم‌پهنای فیلتر مثلثی صاف‌کننده بر حسب درصد ردیف‌ها (حداقل ۱ ردیف).')
nodeProm  = input.float(15.0, 'Advanced: min prominence (% of the highest smoothed row)', minval = 1, maxval = 80, step = 1, group = G5, tooltip = 'قله باید حداقل این‌قدر از دره‌های دو طرفش بلندتر باشد تا HVN حساب شود؛ بین دو HVN هم دره‌ای LVN است که عمقش حداقل این مقدار باشد.')
nodeSep   = input.float(8.0, 'Advanced: min separation between HVNs (% of rows)', minval = 1, maxval = 40, step = 1, group = G5)
nodeZone  = input.float(70.0, 'Advanced: zone = rows above this % of the peak', minval = 30, maxval = 95, step = 5, group = G5)
nodeZones = input.bool(true, 'Advanced: shade node zones', group = G5)
nodeWinPct = input.float(5.0, 'Simple: window (% of rows, each side)', minval = 1, maxval = 25, step = 0.5, group = G5, tooltip = 'فقط برای Simple: قله/دره باید در این بازه (درصدی از تعداد ردیف‌ها، دو طرف؛ حداقل ۲ ردیف) بزرگ‌ترین/کوچک‌ترین باشد.')
hvnPct    = input.float(40.0, 'HVN: min % of the highest (smoothed) row', minval = 10, maxval = 100, step = 5, group = G5)
lvnPct    = input.float(50.0, 'LVN: max % of the highest (smoothed) row', minval = 5, maxval = 90, step = 5, group = G5)
hvnCol    = input.color(C_CYAN, 'HVN', group = G5, inline = 'n1')
lvnCol    = input.color(C_PINK, '', group = G5, inline = 'n1')

nakedOn   = input.bool(true, 'Naked POC (not yet revisited)', group = G6, tooltip = 'POC یک پروفایل تمام‌شده که از آن به بعد قیمت هنوز به آن برنگشته. با اولین لمس به حالت «مصرف‌شده» می‌رود.')
nakedMax  = input.int(6, 'Max active naked POCs shown', minval = 1, maxval = 20, group = G6)
nakedTouch = input.string('Wick', 'Revisit rule', options = ['Wick', 'Close'], group = G6, tooltip = 'Wick: هر سایه‌ای که به سطح بخورد. Close: فقط وقتی کندل از سمت اولیه بسته‌شدن‌اش از سطح عبور کند.')
nakedMaxAge = input.int(0, 'Hide naked POCs older than N bars (0 = never)', minval = 0, maxval = 5000, group = G6)
nakedConsumed = input.int(0, 'Also show the last N consumed POCs (faded)', minval = 0, maxval = 10, group = G6)
nakedProv = input.bool(false, 'Flag a wick touch by the forming bar as provisional', group = G6, tooltip = 'فقط نمایشی: لمس کندل در حال تشکیل هرگز وضعیت تأییدشده را عوض نمی‌کند و فقط با برچسب provisional نشان داده می‌شود.')
nakedCol  = input.color(C_ORANGE, 'Colour', group = G6)

migOn     = input.bool(true, 'POC migration vs previous profile', group = G9)
migTol    = input.float(1.0, 'POC is stable within N rows', minval = 0, maxval = 20, step = 0.5, group = G9)
vaShiftOn = input.bool(true, 'Value Area shift vs previous profile', group = G9)
shapeOn   = input.bool(false, 'Profile shape (heuristic: D / P / b / double distribution)', group = G9, tooltip = 'تقریبی و فقط بر اساس ویژگی‌های عددی (محل POC، چولگی، قله‌ها)؛ اگر اطمینان پایین باشد «Unclassified» می‌دهد. تشخیص ساختار بازار نیست.')
rvolOn    = input.bool(false, 'Relative volume (same elapsed time in previous profiles)', group = G9)
rvolN     = input.int(5, 'Previous profiles averaged', minval = 2, maxval = 20, group = G9)

ibOn      = input.bool(false, 'Show Initial Balance (of the profile period)', group = G10, tooltip = 'IB از اولین کندل دوره محاسبه می‌شود؛ برای معنی‌دار بودن بهتر است پروفایل Session انتخاب شود.')
ibMin     = input.int(60, 'IB duration (minutes)', minval = 5, maxval = 600, group = G10)
ibExt     = input.bool(true, 'Show 1x and 2x extensions', group = G10)
ibCol     = input.color(#AB47BC, 'Colour', group = G10)

confOn    = input.bool(false, 'Confluence zones', group = G11, tooltip = 'جاهایی که چند سطح مهم (POC/VA قبلی، ترکیبی، لنگرشده، IB، Naked POC و HVN) روی هم می‌افتند.')
confTol   = input.float(0.25, 'Cluster tolerance (x ATR14)', minval = 0.05, maxval = 2.0, step = 0.05, group = G11)
confMin   = input.int(3, 'Min levels in a zone', minval = 2, maxval = 8, group = G11)
confMax   = input.int(4, 'Max zones shown (nearest to price)', minval = 1, maxval = 10, group = G11)
confCol   = input.color(#FFD600, 'Colour', group = G11)

compOn    = input.bool(false, 'Composite profile (no double counting)', group = G12, tooltip = 'پروفایل‌های تمام‌شده روی یک شبکه‌ی قیمتی مشترک بازنمونه‌گیری و جمع می‌شوند؛ حجم کل حفظ می‌شود.')
compMode  = input.string('Last N profiles', 'Composite of', options = ['Last N profiles', 'Current week', 'Current month'], group = G12)
compN     = input.int(5, 'N (Last N profiles; the running profile counts as one)', minval = 2, maxval = 40, group = G12)
compRows  = input.int(70, 'Composite rows', minval = 10, maxval = 200, group = G12)
compHist  = input.bool(false, 'Composite histogram (right side)', group = G12)
compCol   = input.color(#009688, 'Colour', group = G12)

ancOn     = input.bool(false, 'Anchored profile (from the anchor to now, chart-bar data)', group = G13, tooltip = 'در TradingView از داده‌ی کندل‌های چارت ساخته می‌شود (حداکثر ۲۰۰۰ کندل اخیر)؛ نسخه‌ی متاتریدر از ریزکندل استفاده می‌کند.')
ancTime   = input.time(0, 'Anchor date / time', group = G13, confirm = false)
ancRows   = input.int(70, 'Anchored rows', minval = 10, maxval = 200, group = G13)
ancHist   = input.bool(false, 'Anchored histogram', group = G13)
ancCol    = input.color(#FF7043, 'Colour', group = G13)

msHist    = input.bool(false, 'Histograms for session profiles (default: lines only)', group = G14)
ms1On     = input.bool(false, 'Session 1 (Asia)', group = G14)
ms1Sess   = input.session('0000-0900', 'Hours', group = G14, inline = 's1')
ms1Tz     = input.string('Asia/Tokyo', '', options = ['Asia/Tokyo', 'Europe/London', 'America/New_York', 'UTC', 'Asia/Tehran', 'Asia/Dubai', 'Europe/Frankfurt', 'Australia/Sydney'], group = G14, inline = 's1')
ms1Col    = input.color(#AB47BC, '', group = G14, inline = 's1')
ms2On     = input.bool(false, 'Session 2 (London)', group = G14)
ms2Sess   = input.session('0800-1630', 'Hours', group = G14, inline = 's2')
ms2Tz     = input.string('Europe/London', '', options = ['Asia/Tokyo', 'Europe/London', 'America/New_York', 'UTC', 'Asia/Tehran', 'Asia/Dubai', 'Europe/Frankfurt', 'Australia/Sydney'], group = G14, inline = 's2')
ms2Col    = input.color(#2962FF, '', group = G14, inline = 's2')
ms3On     = input.bool(false, 'Session 3 (New York)', group = G14)
ms3Sess   = input.session('0930-1600', 'Hours', group = G14, inline = 's3')
ms3Tz     = input.string('America/New_York', '', options = ['Asia/Tokyo', 'Europe/London', 'America/New_York', 'UTC', 'Asia/Tehran', 'Asia/Dubai', 'Europe/Frankfurt', 'Australia/Sydney'], group = G14, inline = 's3')
ms3Col    = input.color(#26A65B, '', group = G14, inline = 's3')
ms4On     = input.bool(false, 'Session 4 (custom)', group = G14)
ms4Sess   = input.session('2200-0600', 'Hours', group = G14, inline = 's4')
ms4Tz     = input.string('UTC', '', options = ['Asia/Tokyo', 'Europe/London', 'America/New_York', 'UTC', 'Asia/Tehran', 'Asia/Dubai', 'Europe/Frankfurt', 'Australia/Sydney'], group = G14, inline = 's4')
ms4Col    = input.color(#FF9800, '', group = G14, inline = 's4')

showTable = input.bool(true, 'Show table', group = G7)
tablePos  = input.string(position.top_right, 'Position', options = [position.top_right, position.top_left, position.bottom_right, position.bottom_left], group = G7)
tableSizeStr = input.string('tiny', 'Text', options = ['tiny', 'small', 'normal', 'large'], group = G7)

showWatermark = input.bool(true, 'Show crypex.org watermark', group = G8)
wmPos     = input.string(position.bottom_center, 'Position', options = [position.bottom_left, position.bottom_center, position.bottom_right, position.middle_center, position.top_center, position.top_left], group = G8)
wmGap     = input.float(5.0, 'Gap from left (%)', minval = 0, maxval = 30, step = 0.5, group = G8, tooltip = 'Bottom-left only: empty space (in % of chart width) so the watermark sits right beside the TradingView logo instead of on top of it.')
wmSizeStr = input.string('large', 'Size', options = ['small', 'normal', 'large', 'huge'], group = G8)
wmTransp  = input.int(0, 'Transparency', minval = 0, maxval = 95, group = G8, tooltip = '0 = exact logo colours, 95 = barely visible.')

vaMode    = vaMethod == 'Pair · 2 rows (CME / Dalton)' ? 2 : vaMethod == 'Target-reached · 1 row' ? 1 : 0
tableSize = tableSizeStr == 'tiny' ? size.tiny : tableSizeStr == 'normal' ? size.normal : tableSizeStr == 'large' ? size.large : size.small
lblSize   = lblSizeStr == 'tiny' ? size.tiny : lblSizeStr == 'normal' ? size.normal : lblSizeStr == 'large' ? size.large : size.small

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// TYPE
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Row i covers prices [off + (base + i) * rs, off + (base + i + 1) * rs).
type Prof
    int          startBar
    int          endBar
    int          startTime
    int          endTime
    int          touchTime
    float        rs
    float        off
    int          base
    array<float> up
    array<float> dn
    float        poc
    float        vah
    float        val
    float        total
    float        upTotal
    float        hi
    float        lo
    bool         naked
    int          touchBar
    float        side
    float        ibH
    float        ibL
    bool         ibApprox
    int          qI
    int          qF
    int          qS
    int          qExp
    int          qGot
    int          qPart
    int          qTm
    float        qVolIn
    float        qVolChart
    array<float> cum
    array<int>   cumT

newProf(int sb, int st, float rs, float off) =>
    Prof.new(startBar = sb, endBar = sb, startTime = st, endTime = st, touchTime = na, rs = rs, off = off, base = 0, up = array.new_float(), dn = array.new_float(), poc = na, vah = na, val = na, total = 0.0, upTotal = 0.0, hi = na, lo = na, naked = true, touchBar = na, side = 1.0, ibH = na, ibL = na, ibApprox = false, qI = 0, qF = 0, qS = 0, qExp = 0, qGot = 0, qPart = 0, qTm = 0, qVolIn = 0.0, qVolChart = 0.0, cum = array.new_float(), cumT = array.new_int())

type Nodes
    array<int>   hI
    array<float> hH
    array<int>   hZl
    array<int>   hZh
    array<int>   lI
    array<float> lH
    array<int>   lZl
    array<int>   lZh
    array<float> sm

type Zones
    array<float>  lo
    array<float>  hi
    array<int>    n
    array<string> s

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// FUNCTIONS
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// CRX-019: hard cap on rows per profile. When a spike would exceed it, the grid is coarsened by an INTEGER factor m (new row = m old rows, same origin),
// so every new boundary is an old boundary: the volume is conserved exactly, the row size stays a whole multiple of the tick, and the result is reproducible.
MAX_ROWS = 2000

regrid(Prof p, int m) =>
    int n = p.up.size()
    if n > 0
        int nb = int(math.floor(p.base * 1.0 / m))
        int ne = int(math.floor((p.base + n - 1) * 1.0 / m))
        array<float> u = array.new_float(ne - nb + 1, 0.0)
        array<float> d = array.new_float(ne - nb + 1, 0.0)
        for i = 0 to n - 1
            int j = int(math.floor((p.base + i) * 1.0 / m)) - nb
            u.set(j, u.get(j) + p.up.get(i))
            d.set(j, d.get(j) + p.dn.get(i))
        p.up := u
        p.dn := d
        p.base := nb
    p.rs := p.rs * m

// Spread one candle's volume uniformly over its High-Low range into the price rows (row share = overlap / range).
addVol(Prof p, float h, float l, float v, bool isUp) =>
    if not na(h) and not na(l) and not na(v) and v > 0 and h >= l
        float rs = p.rs
        float posL = (l - p.off) / rs
        float posH = (h - p.off) / rs
        int i0 = math.floor(posL + 1e-9)
        int i1 = math.floor(posH + 1e-9)
        int lowI = p.up.size() == 0 ? i0 : math.min(i0, p.base)
        int hiI = p.up.size() == 0 ? i1 : math.max(i1, p.base + p.up.size() - 1)
        if hiI - lowI + 1 > MAX_ROWS
            regrid(p, int(math.ceil((hiI - lowI + 1) * 1.0 / (MAX_ROWS - 1))))
            rs := p.rs
            posL := (l - p.off) / rs
            posH := (h - p.off) / rs
            i0 := math.floor(posL + 1e-9)
            i1 := math.floor(posH + 1e-9)
        if i1 > i0 and posH - i1 < 1e-9     // High exactly on a row boundary: the row above gets nothing
            i1 -= 1
        if p.up.size() == 0
            p.base := i0
            p.up.push(0.0)
            p.dn.push(0.0)
        while i0 < p.base
            p.up.unshift(0.0)
            p.dn.unshift(0.0)
            p.base := p.base - 1
        while i1 > p.base + p.up.size() - 1
            p.up.push(0.0)
            p.dn.push(0.0)
        float rng = h - l
        if rng <= 0 or i0 == i1
            int k = i0 - p.base
            if isUp
                p.up.set(k, p.up.get(k) + v)
            else
                p.dn.set(k, p.dn.get(k) + v)
        else
            for i = i0 to i1
                float rl = p.off + i * rs
                float ov = math.min(h, rl + rs) - math.max(l, rl)
                if ov > 0
                    int k = i - p.base
                    float share = v * ov / rng
                    if isUp
                        p.up.set(k, p.up.get(k) + share)
                    else
                        p.dn.set(k, p.dn.get(k) + share)
        p.total := p.total + v
        if isUp
            p.upTotal := p.upTotal + v
        p.hi := na(p.hi) ? h : math.max(p.hi, h)
        p.lo := na(p.lo) ? l : math.min(p.lo, l)

// POC + Value Area, returned as row indexes [pocIdx, lowIdx, highIdx] (-1 when empty).
// POC = highest-volume row; equal volumes (within 1e-12 of the profile total) -> the row closest to the profile centre, then the lower one.
// vaMode 0 = TradingView documented rule, 1 = target-reached (1 row), 2 = pair (2 rows).
calcVA(Prof p, float pct, int mode) =>
    int n = p.up.size()
    int pocI = -1
    int lo = 0
    int hi = 0
    if n > 0 and p.total > 0
        float eps = p.total * 1e-12
        float best = -1.0
        float mid = (n - 1) / 2.0
        for i = 0 to n - 1
            float t = p.up.get(i) + p.dn.get(i)
            if t > best + eps
                best := t
                pocI := i
            else if math.abs(t - best) <= eps and math.abs(i - mid) < math.abs(pocI - mid)
                best := math.max(best, t)
                pocI := i
        float target = p.total * pct / 100.0
        lo := pocI
        hi := pocI
        if mode == 0
            float remaining = target - best
            while remaining > eps and (lo > 0 or hi < n - 1)
                bool hasA = hi < n - 1
                bool hasB = lo > 0
                float aV = hasA ? p.up.get(hi + 1) + p.dn.get(hi + 1) : -1.0
                float bV = hasB ? p.up.get(lo - 1) + p.dn.get(lo - 1) : -1.0
                bool takeAbove = false
                if not hasB
                    takeAbove := true
                else if not hasA
                    takeAbove := false
                else if aV > bV + eps
                    takeAbove := true
                else if bV > aV + eps
                    takeAbove := false
                else
                    takeAbove := (hi + 1 - pocI) <= (pocI - (lo - 1))   // tie: the row closer to the POC, then the upper one
                float v = takeAbove ? aV : bV
                if v > remaining + eps
                    break                                               // documented stop rule: the row would exceed the remaining target
                remaining -= v
                if takeAbove
                    hi += 1
                else
                    lo -= 1
        else
            float acc = best
            int steps = mode == 2 ? 2 : 1
            while acc + eps < target and (lo > 0 or hi < n - 1)
                float aboveV = 0.0
                float belowV = 0.0
                int aboveN = 0
                int belowN = 0
                for s = 1 to steps
                    if hi + s <= n - 1
                        aboveV += p.up.get(hi + s) + p.dn.get(hi + s)
                        aboveN += 1
                    if lo - s >= 0
                        belowV += p.up.get(lo - s) + p.dn.get(lo - s)
                        belowN += 1
                bool takeAbove = false
                if aboveN == 0
                    takeAbove := false
                else if belowN == 0
                    takeAbove := true
                else if aboveV > belowV + eps
                    takeAbove := true
                else if belowV > aboveV + eps
                    takeAbove := false
                else
                    takeAbove := (hi + 1 - pocI) <= (pocI - (lo - 1))
                if takeAbove
                    for s = 1 to aboveN
                        acc += p.up.get(hi + s) + p.dn.get(hi + s)
                    hi += aboveN
                else
                    for s = 1 to belowN
                        acc += p.up.get(lo - s) + p.dn.get(lo - s)
                    lo -= belowN
    [pocI, lo, hi]

setLevels(Prof p) =>
    [pocI, lo, hi] = calcVA(p, vaPct, vaMode)
    if pocI >= 0
        p.poc := p.off + (p.base + pocI + 0.5) * p.rs
        p.vah := p.off + (p.base + hi + 1) * p.rs
        p.val := p.off + (p.base + lo) * p.rs
    pocI

// HVN = local maxima of the 1-2-1 smoothed profile (leftmost row wins on a plateau) holding >= hvnPct of the POC volume.
// LVN = local minima holding <= lvnPct of the POC volume that lie between two HVNs.
findNodes(Prof p) =>
    array<int> hv = array.new_int()
    array<int> lv = array.new_int()
    int n = p.up.size()
    int nodeWin = math.max(2, math.round(n * nodeWinPct / 100.0))
    if n >= 5
        array<float> sm = array.new_float(n, 0.0)
        float smMax = 0.0
        for i = 0 to n - 1
            float b = p.up.get(i) + p.dn.get(i)
            float a = i > 0 ? p.up.get(i - 1) + p.dn.get(i - 1) : b
            float c = i < n - 1 ? p.up.get(i + 1) + p.dn.get(i + 1) : b
            float s = (a + 2 * b + c) / 4.0
            sm.set(i, s)
            smMax := math.max(smMax, s)
        if smMax > 0
            array<int> lvRaw = array.new_int()
            for i = 0 to n - 1
                float s = sm.get(i)
                bool isMax = true
                bool isMin = true
                for j = math.max(0, i - nodeWin) to math.min(n - 1, i + nodeWin)
                    if j != i
                        float v = sm.get(j)
                        if v > s or (v == s and j < i)
                            isMax := false
                        if v < s or (v == s and j < i)
                            isMin := false
                if isMax and s >= smMax * hvnPct / 100.0
                    hv.push(i)
                if isMin and s <= smMax * lvnPct / 100.0 and i >= nodeWin and i <= n - 1 - nodeWin
                    lvRaw.push(i)
            if hv.size() >= 2 and lvRaw.size() > 0
                for k = 0 to lvRaw.size() - 1
                    int li = lvRaw.get(k)
                    if li > hv.get(0) and li < hv.get(hv.size() - 1)
                        lv.push(li)
    [hv, lv]

// ---- advanced analytics ----------------------------------------------------------------------------------------------
smoothRows(Prof p) =>
    int n = p.up.size()
    int s = math.max(1, math.floor(n * nodeSmooth / 100.0 + 0.5))
    array<float> sm = array.new_float(n, 0.0)
    for i = 0 to n - 1
        float num = 0.0
        float den = 0.0
        for d = -s to s
            int j = i + d
            if j >= 0 and j < n
                float w = s + 1 - math.abs(d)
                num += w * (p.up.get(j) + p.dn.get(j))
                den += w
        sm.set(i, num / den)
    sm

peakProm(array<float> sm, int n, int p, float eps) =>
    float h = sm.get(p)
    float lmin = h
    float rmin = h
    int L = p - 1
    bool goL = L >= 0
    while goL
        if sm.get(L) <= h + eps
            lmin := math.min(lmin, sm.get(L))
            L -= 1
            goL := L >= 0
        else
            goL := false
    int R = p + 1
    bool goR = R < n
    while goR
        if sm.get(R) <= h + eps
            rmin := math.min(rmin, sm.get(R))
            R += 1
            goR := R < n
        else
            goR := false
    // CRX-023: a side with nothing lower than the peak (edge of the profile) does not define a col
    bool hasL = lmin < h - eps
    bool hasR = rmin < h - eps
    hasL and hasR ? h - math.max(lmin, rmin) : hasL ? h - lmin : hasR ? h - rmin : 0.0

// Advanced nodes: HVN = prominent peaks (plateau -> centre) with a minimum separation; LVN = valley between consecutive HVNs.
advNodes(Prof p) =>
    Nodes r = Nodes.new(hI = array.new_int(), hH = array.new_float(), hZl = array.new_int(), hZh = array.new_int(), lI = array.new_int(), lH = array.new_float(), lZl = array.new_int(), lZh = array.new_int(), sm = array.new_float())
    int n = p.up.size()
    if n >= 5
        array<float> sm = smoothRows(p)
        r.sm := sm
        float mx = sm.max()
        if mx > 0
            float eps = mx * 1e-12
            array<int> peaks = array.new_int()
            int i = 0
            while i < n
                int j = i
                bool grow = j + 1 < n
                while grow
                    if math.abs(sm.get(j + 1) - sm.get(i)) <= eps
                        j += 1
                        grow := j + 1 < n
                    else
                        grow := false
                bool leftOk = i == 0
                if i > 0
                    leftOk := sm.get(i - 1) < sm.get(i) - eps
                bool rightOk = j == n - 1
                if j < n - 1
                    rightOk := sm.get(j + 1) < sm.get(i) - eps
                if leftOk and rightOk and not (i == 0 and j == n - 1)
                    peaks.push(math.floor((i + j) / 2))
                i := j + 1
            array<int> cand = array.new_int()
            if peaks.size() > 0
                for q = 0 to peaks.size() - 1
                    int pk = peaks.get(q)
                    if peakProm(sm, n, pk, eps) >= nodeProm / 100.0 * mx - eps and sm.get(pk) >= hvnPct / 100.0 * mx - eps
                        cand.push(pk)
            int nc = cand.size()
            // strongest first (ties: lower row)
            if nc > 1
                for a = 1 to nc - 1
                    int v = cand.get(a)
                    int b = a - 1
                    bool go = true
                    while go and b >= 0
                        int cb = cand.get(b)
                        if sm.get(cb) < sm.get(v) or (sm.get(cb) == sm.get(v) and cb > v)
                            cand.set(b + 1, cb)
                            b -= 1
                        else
                            go := false
                    cand.set(b + 1, v)
            int sep = math.max(1, math.floor(n * nodeSep / 100.0 + 0.5))
            array<int> acc = array.new_int()
            if nc > 0
                for q = 0 to nc - 1
                    bool okp = true
                    if acc.size() > 0
                        for rr = 0 to acc.size() - 1
                            if math.abs(cand.get(q) - acc.get(rr)) < sep
                                okp := false
                    if okp
                        acc.push(cand.get(q))
            int nAcc = acc.size()
            if nAcc > 1
                for a = 1 to nAcc - 1
                    int v = acc.get(a)
                    int b = a - 1
                    bool go = true
                    while go and b >= 0
                        if acc.get(b) > v
                            acc.set(b + 1, acc.get(b))
                            b -= 1
                        else
                            go := false
                    acc.set(b + 1, v)
            if nAcc > 0
                for q = 0 to nAcc - 1
                    int pk = acc.get(q)
                    float thr = nodeZone / 100.0 * sm.get(pk)
                    int zl = pk
                    bool gl = zl - 1 >= 0
                    while gl
                        if sm.get(zl - 1) >= thr
                            zl -= 1
                            gl := zl - 1 >= 0
                        else
                            gl := false
                    int zh = pk
                    bool gh = zh + 1 < n
                    while gh
                        if sm.get(zh + 1) >= thr
                            zh += 1
                            gh := zh + 1 < n
                        else
                            gh := false
                    r.hI.push(pk)
                    r.hH.push(sm.get(pk))
                    r.hZl.push(zl)
                    r.hZh.push(zh)
            // CRX-023: neighbouring HVN zones must not overlap - the border is the lowest row between the two peaks
            if nAcc > 1
                for q = 0 to nAcc - 2
                    int za = acc.get(q)
                    int zb = acc.get(q + 1)
                    int vb = za
                    for x = za to zb
                        if sm.get(x) < sm.get(vb)
                            vb := x
                    if r.hZh.get(q) > vb
                        r.hZh.set(q, vb)
                    if r.hZl.get(q + 1) <= vb
                        r.hZl.set(q + 1, math.min(vb + 1, acc.get(q + 1)))
                    if r.hZh.get(q) >= r.hZl.get(q + 1)
                        r.hZh.set(q, r.hZl.get(q + 1) - 1)
            if nAcc > 1
                for q = 0 to nAcc - 2
                    int a = acc.get(q)
                    int b = acc.get(q + 1)
                    float vmin = sm.get(a)
                    for x = a to b
                        vmin := math.min(vmin, sm.get(x))
                    int first = a
                    for x = b to a
                        if sm.get(x) == vmin
                            first := x
                    int last = b
                    for x = a to b
                        if sm.get(x) == vmin
                            last := x
                    int v = math.floor((first + last) / 2)
                    float pkLow = math.min(sm.get(a), sm.get(b))
                    if vmin <= lvnPct / 100.0 * mx + eps and pkLow - vmin >= nodeProm / 100.0 * mx - eps
                        float lim = vmin + 0.25 * (pkLow - vmin)
                        int zl = v
                        bool gl = zl - 1 > a
                        while gl
                            if sm.get(zl - 1) <= lim
                                zl -= 1
                                gl := zl - 1 > a
                            else
                                gl := false
                        int zh = v
                        bool gh = zh + 1 < b
                        while gh
                            if sm.get(zh + 1) <= lim
                                zh += 1
                                gh := zh + 1 < b
                            else
                                gh := false
                        r.lI.push(v)
                        r.lH.push(vmin)
                        r.lZl.push(zl)
                        r.lZh.push(zh)
    r

skewRows(Prof p) =>
    int n = p.up.size()
    float tot = 0.0
    float m = 0.0
    for i = 0 to n - 1
        float t = p.up.get(i) + p.dn.get(i)
        tot += t
        m += (i + 0.5) * t
    float res = 0.0
    if tot > 0
        m := m / tot
        float vr = 0.0
        float c3 = 0.0
        for i = 0 to n - 1
            float t = p.up.get(i) + p.dn.get(i)
            float d = (i + 0.5) - m
            vr += t * d * d
            c3 += t * d * d * d
        vr := vr / tot
        if vr > 1e-18
            res := c3 / tot / math.pow(math.sqrt(vr), 3)
    res

// Heuristic only: D / P / b / double distribution, otherwise Unclassified (never a market-structure claim).
shapeOf(Prof p, int pocI) =>
    int n = p.up.size()
    string r = 'Unclassified'
    if n < 15 or p.total <= 0
        r := 'Unclassified (thin)'
    else
        Nodes nd = advNodes(p)
        int K = nd.hI.size()
        bool done = false
        if K >= 2
            int b1 = 0
            for q = 1 to K - 1
                if nd.hH.get(q) > nd.hH.get(b1)
                    b1 := q
            int b2 = -1
            for q = 0 to K - 1
                if q != b1
                    if b2 < 0
                        b2 := q
                    else if nd.hH.get(q) > nd.hH.get(b2)
                        b2 := q
            int pa = math.min(nd.hI.get(b1), nd.hI.get(b2))
            int pb = math.max(nd.hI.get(b1), nd.hI.get(b2))
            float h1 = nd.hI.get(b1) == pa ? nd.hH.get(b1) : nd.hH.get(b2)
            float h2 = nd.hI.get(b1) == pa ? nd.hH.get(b2) : nd.hH.get(b1)
            float valley = nd.sm.get(pa)
            for x = pa to pb
                valley := math.min(valley, nd.sm.get(x))
            if (pb - pa) / n >= 0.25 and valley / math.min(h1, h2) <= 0.6
                r := 'Double distribution'
                done := true
        if not done
            float pp = (pocI + 0.5) / n
            float sk = skewRows(p)
            if pp >= 0.62 and sk <= -0.3
                r := 'P-shape'
            else if pp <= 0.38 and sk >= 0.3
                r := 'b-shape'
            else if pp > 0.38 and pp < 0.62 and math.abs(sk) < 0.3 and K <= 2
                r := 'D-shape'
    r

vaShiftText(float cVal, float cVah, float pVal, float pVah) =>
    float e = 1e-9
    string r = ''
    if cVal > pVah + e
        r := 'Higher value'
    else if cVah < pVal - e
        r := 'Lower value'
    else if cVal >= pVal - e and cVah <= pVah + e
        r := 'Inside previous VA'
    else if cVal < pVal - e and cVah > pVah + e
        r := 'Outside previous VA'
    else
        r := (cVal + cVah) > (pVal + pVah) ? 'Overlapping higher' : 'Overlapping lower'
    r

migState(float d, float rs) =>
    math.abs(d) <= migTol * rs + 1e-12 ? 'Stable' : d > 0 ? 'Migrating up' : 'Migrating down'

// CRX-011: relative volume = ACCEPTED profile volume so far / mean of the previous rvolN profiles at the same ELAPSED TIME since each
// profile's own first bar (not the same bar ordinal). Profiles that are too short to reach that elapsed time are left out.
rvolOf(Prof c, array<Prof> arr) =>
    int kk = c.cum.size()
    float rv = -1.0
    int used = 0
    if kk > 0
        float curV = c.cum.get(kk - 1)
        int el = c.cumT.get(kk - 1)
        float sum = 0.0
        int seen = 0
        int q = arr.size() - 1
        while q >= 0 and seen < rvolN
            Prof pq = arr.get(q)
            int pn = pq.cum.size()
            if pn > 0 and pq.cumT.get(pn - 1) >= el
                int jj = -1
                for j = 0 to pn - 1
                    if pq.cumT.get(j) <= el
                        jj := j
                    else
                        break
                if jj >= 0
                    sum += pq.cum.get(jj)
                    used += 1
            seen += 1
            q -= 1
        if used >= 2
            float a = sum / used
            if a > 0
                rv := curV / a
    [rv, used]

// clusters of consecutive sorted levels whose neighbour gap <= tol; keeps clusters with at least minc levels
clusterLevels(array<float> pr, array<string> nm, array<string> sk, float tol, int minc) =>
    // CRX-022: window of at most `tol` width (no chaining) counting DISTINCT sources (one profile = one source)
    Zones z = Zones.new(lo = array.new_float(), hi = array.new_float(), n = array.new_int(), s = array.new_string())
    int cnt = pr.size()
    if cnt > 0
        array<int> idx = array.sort_indices(pr, order.ascending)
        int st = 0
        while st < cnt
            int en = st
            while en + 1 < cnt and pr.get(idx.get(en + 1)) - pr.get(idx.get(st)) <= tol + 1e-12
                en += 1
            string seen = '|'
            int dist = 0
            string lab = ''
            for x = st to en
                string key = sk.get(idx.get(x))
                if not str.contains(seen, '|' + key + '|')
                    seen += key + '|'
                    dist += 1
                    if dist <= 4
                        lab += (dist > 1 ? '+' : '') + nm.get(idx.get(x))
            if dist >= minc
                z.lo.push(pr.get(idx.get(st)))
                z.hi.push(pr.get(idx.get(en)))
                z.n.push(dist)
                z.s.push(lab)
                st := en + 1
            else
                st += 1
    z

// composite: every source row keeps its volume spread uniformly over its own price span; composite rows take the overlapping share
// CRX-017: data quality of ONE profile (an old, poorly covered profile must not hide behind a good global average)
qualTxt(Prof p) =>
    string t = '—'
    if not na(p)
        int n = p.qI + p.qF + p.qS
        if n > 0
            t := str.tostring(p.qI * 100.0 / n, '#') + '% intrabar'
            t += p.qExp > 0 ? ' · time ' + str.tostring(p.qGot * 100.0 / p.qExp, '#.0') + '%' : ''
            t += p.qVolChart > 0 ? ' · vol ' + str.tostring(p.qVolIn * 100.0 / p.qVolChart, '#.0') + '%' : ''
            t += p.qF > 0 ? ' · ' + str.tostring(p.qF) + ' chart-bar' : ''
            t += p.qS > 0 ? ' · ' + str.tostring(p.qS) + ' MISSING' : ''
            t += p.qPart > 0 ? ' · ' + str.tostring(p.qPart) + ' PARTIAL' : ''
            t += p.qTm > 0 ? ' · ' + str.tostring(p.qTm) + ' vol-type mismatch' : ''
        else
            t := 'chart bars'
    t

qualPoor(Prof p) =>
    not na(p) and (p.qS > 0 or p.qPart > 0 or p.qTm > 0 or p.qF > 0 or (p.qExp > 0 and p.qGot < p.qExp * 0.9) or (p.qVolChart > 0 and p.qVolIn < p.qVolChart * 0.95))

makeComposite(array<Prof> src, int rowsWanted) =>
    int ns = src.size()
    Prof c = na
    if ns > 0
        float lo = 1e300
        float hi = -1e300
        for q = 0 to ns - 1
            Prof p = src.get(q)
            lo := math.min(lo, p.off + p.base * p.rs)
            hi := math.max(hi, p.off + (p.base + p.up.size()) * p.rs)
        float rs = math.max(syminfo.mintick, math.round((hi - lo) / math.max(1, rowsWanted) / syminfo.mintick) * syminfo.mintick)   // CRX-014: whole multiple of the tick
        int n = math.max(1, math.ceil((hi - lo) / rs - 1e-9))
        Prof f = src.get(0)
        c := newProf(f.startBar, f.startTime, rs, lo)
        c.up := array.new_float(n, 0.0)
        c.dn := array.new_float(n, 0.0)
        c.hi := -1e300
        c.lo := 1e300
        c.endBar := f.endBar
        for q = 0 to ns - 1
            Prof p = src.get(q)
            c.qI += p.qI
            c.qF += p.qF
            c.qS += p.qS
            c.qExp += p.qExp
            c.qGot += p.qGot
            c.qPart += p.qPart
            c.qTm += p.qTm
            c.qVolIn += p.qVolIn
            c.qVolChart += p.qVolChart
            for i = 0 to p.up.size() - 1
                float vu = p.up.get(i)
                float vd = p.dn.get(i)
                if vu > 0 or vd > 0
                    float b = p.off + (p.base + i) * p.rs
                    float tp = b + p.rs
                    int j0 = math.max(0, math.floor((b - lo) / rs + 1e-9))
                    int j1 = math.min(n - 1, math.floor((tp - lo) / rs - 1e-9))
                    if j1 < j0
                        j1 := j0
                    float S = 0.0
                    for j = j0 to j1
                        S += math.max(0.0, math.min(tp, lo + (j + 1) * rs) - math.max(b, lo + j * rs))
                    for j = j0 to j1
                        float ov = math.max(0.0, math.min(tp, lo + (j + 1) * rs) - math.max(b, lo + j * rs))
                        float fr = S > 0 ? ov / S : (j == j0 ? 1.0 : 0.0)
                        c.up.set(j, c.up.get(j) + vu * fr)
                        c.dn.set(j, c.dn.get(j) + vd * fr)
            c.total := c.total + p.total
            c.upTotal := c.upTotal + p.upTotal
            c.hi := math.max(c.hi, p.hi)
            c.lo := math.min(c.lo, p.lo)
            if p.startTime < c.startTime
                c.startTime := p.startTime
                c.startBar := p.startBar
            if p.endBar > c.endBar
                c.endBar := p.endBar
        setLevels(c)
    c

inSessAt(int t, int sS, int sE, string tz) =>
    int m = hour(t, tz) * 60 + minute(t, tz)
    sS < sE ? (m >= sS and m < sE) : (m >= sS or m < sE)

// CRX-007: session ID = local calendar date (in the session's time zone) of the day the session STARTED, packed as yyyymmdd.
// An overnight session (e.g. 22:00-06:00) belongs to the date it started on; a 24h session (start == end) rolls over at its start time.
// Detecting 'out of session -> in session' alone merges several days into one profile when the data has no off-session bars.
sessIdAt(int t, int sS, int sE, string tz) =>
    int m = hour(t, tz) * 60 + minute(t, tz)
    int tt = sS >= sE and m < sE ? t - 86400000 : t
    year(tt, tz) * 10000 + month(tt, tz) * 100 + dayofmonth(tt, tz)

// CRX-028: share of the chart bar [t, t+csec) that lies inside the session [sS, sE) (minutes of the local day; sS >= sE = overnight)
ovRange(float a0, float a1, float lo, float hi) =>
    math.max(0.0, math.min(a1, hi) - math.max(a0, lo))

sessFrac(int t, int sS, int sE, string tz, int csec) =>
    float m0 = hour(t, tz) * 60 + minute(t, tz)
    float m1 = m0 + csec / 60.0
    float cov = sS < sE ? ovRange(m0, m1, sS, sE) : math.max(ovRange(m0, m1, sS, 1440) + ovRange(m0, m1, 0, sE), math.max(ovRange(m0, m1, sS - 1440, sE), ovRange(m0, m1, sS, sE + 1440)))
    math.min(1.0, math.max(0.0, cov / (csec / 60.0)))

autoLtf(int sec) =>
    string r = ''
    if sec > 60
        r := sec / 60 <= 100 ? '1' : sec / 180 <= 100 ? '3' : sec / 300 <= 100 ? '5' : sec / 900 <= 100 ? '15' : sec / 1800 <= 100 ? '30' : sec / 3600 <= 100 ? '60' : '240'
    r

fmtPrice(float x) =>
    na(x) ? '—' : str.tostring(x, format.mintick)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// PERIOD DETECTION & INTRABAR DATA
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
int chartSec = timeframe.in_seconds(timeframe.period)
// Auto: picks the period that always has finer candles inside it than the chart itself, so the
// "Chart TF ≥ period" guard never blanks the indicator, no matter what timeframe it is added on.
string effMode = mode != M_AUTO ? mode : (chartSec < 86400 ? M_DAY : chartSec < 604800 ? M_WEEK : chartSec < 2592000 ? M_MONTH : M_ROLL)
string ltfStr = volSrc == 'Chart bars only' or effMode == M_ROLL ? '' : (ltfIn == 'Auto' ? autoLtf(chartSec) : ltfIn)
bool useLtf = ltfStr != ''
[lh, ll, lvv, lop, lcl, ltm] = request.security_lower_tf(syminfo.tickerid, useLtf ? ltfStr : timeframe.period, [high, low, volume, open, close, time], ignore_invalid_timeframe = true)

bool inSess = not na(time(timeframe.period, sessStr, sessTz))
// session limits in minutes of the day (first 9 characters of the session string, e.g. '0800-1600'); used to filter intrabars at the session edges
int sessS = int(str.tonumber(str.substring(sessStr, 0, 2))) * 60 + int(str.tonumber(str.substring(sessStr, 2, 4)))
int sessE = int(str.tonumber(str.substring(sessStr, 5, 7))) * 60 + int(str.tonumber(str.substring(sessStr, 7, 9)))
inSessMin(int t) =>
    int m = hour(t, sessTz) * 60 + minute(t, sessTz)
    sessS < sessE ? (m >= sessS and m < sessE) : (m >= sessS or m < sessE)

// adds the bar that just CLOSED to profile p: intrabars when they add up to the chart bar, otherwise the chart bar itself (never both)
// Runtime fix: request.security_lower_tf() returning a tuple of arrays does not guarantee every array in the
// tuple has the same size on every bar (seen on bar 0 of a fresh chart: lh had 1 element while ltm had 0,
// crashing array.get(0) on ltm). Always bound any loop by the SMALLEST of the arrays actually indexed together,
// never by lh.size() alone.
ltfN() =>
    useLtf and not na(lh) and not na(ll) and not na(lvv) and not na(lop) and not na(lcl) and not na(ltm) ? math.min(math.min(math.min(lh.size(), ll.size()), math.min(lvv.size(), lop.size())), math.min(lcl.size(), ltm.size())) : 0

feedBar(Prof p, bool filt, int sS, int sE, string tz) =>
    int rI = 0
    int rF = 0
    int rS = 0
    float accV = 0.0
    int cvExp = 0
    int cvGot = 0
    int cvSparse = 0
    int cvPartial = 0
    int cvTypeMis = 0
    float cvIn = 0.0
    float cvChart = 0.0
    int m = ltfN()
    bool useIntra = false
    if m > 0
        float sumL = lvv.sum()
        float chartV = nz(volume)
        bool volMatch = (chartV <= 0 and sumL <= 0) or (chartV > 0 and math.abs(sumL - chartV) <= chartV * volTol / 100.0)
        useIntra := volSrc == 'Strict intrabars' ? true : volMatch
        // CRX-008: time coverage = LTF slots present / slots expected; volume coverage = intrabar volume / chart-bar volume.
        // Missing slots whose volume still matches the chart bar are minutes without trades, not lost data.
        int expN = math.max(1, int(chartSec / math.max(1, timeframe.in_seconds(ltfStr))))
        cvExp := expN
        cvGot := math.min(expN, m)
        cvSparse := volMatch and m < expN ? 1 : 0
        cvPartial := not volMatch and volSrc == 'Strict intrabars' ? 1 : 0
        cvTypeMis := chartV > 0 and sumL <= 0 ? 1 : 0
        if chartV > 0
            cvChart := chartV
            cvIn := math.min(chartV, sumL)
    if useIntra
        for k = 0 to m - 1
            bool okIn = true
            if filt
                okIn := inSessAt(ltm.get(k), sS, sE, tz)
            if okIn
                addVol(p, lh.get(k), ll.get(k), lvv.get(k), lcl.get(k) >= lop.get(k))
                accV += lvv.get(k)
        rI := 1
    else if volSrc == 'Strict intrabars' and useLtf
        rS := 1
    else
        // CRX-028: no usable intrabars. A bar only partly inside the session must never be added whole (volume outside the session).
        float fr = filt ? sessFrac(time, sS, sE, tz, chartSec) : 1.0
        if fr >= 0.999
            addVol(p, high, low, volume, close >= open)
            accV := nz(volume)
        else
            cvPartial := 1                                         // flagged: this profile has an incomplete boundary bar
            if sessBound == 'Estimated partial boundary' and fr > 0
                addVol(p, high, low, volume * fr, close >= open)
                accV := nz(volume) * fr
        rF := 1
    [rI, rF, rS, cvExp, cvGot, cvSparse, cvPartial, cvTypeMis, cvIn, cvChart, accV]

int sessIdNow = inSess ? sessIdAt(time, sessS, sessE, sessTz) : na
bool pNew = false
bool pEnd = false
bool pActive = true
if effMode == M_DAY
    pNew := timeframe.change('D')
else if effMode == M_WEEK
    pNew := timeframe.change('W')
else if effMode == M_MONTH
    pNew := timeframe.change('M')
else if effMode == M_SESS
    bool sessIdChanged = inSess and inSess[1] and sessIdNow != nz(sessIdNow[1], sessIdNow)
    pNew := inSess and (not inSess[1] or sessIdChanged)
    pEnd := (not inSess and inSess[1]) or sessIdChanged
    pActive := inSess

// CRX-004: ta.atr() is na for the first bars of a symbol's history (fewer bars than the ATR length). A na row size would make
// every row index na -> a runtime error on math.floor(na) when converted to int. Fall back to a deterministic, always-positive,
// always-finite range (0.1% of price, floored at 10 mintick) so the FIRST profile of a short history never gets an invalid grid.
float rangeFallback = math.max(syminfo.mintick * 10, close * 0.001)
float bootRngRaw = nz(ta.atr(20), rangeFallback)
float bootRng = (bootRngRaw > 0 ? bootRngRaw : rangeFallback) * 3.0
float atr14 = nz(ta.atr(14), rangeFallback)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// STATE
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var Prof cur = na
var array<Prof> profs = array.new<Prof>()
var array<float> rngHist = array.new_float()
var int ltfCnt = 0
var int fbCnt = 0
var int skipCnt = 0
var int covExp = 0
var int covGot = 0
var int covSparse = 0
var int covPartial = 0
var int covTypeMis = 0
var float covVolIn = 0.0
var float covVolChart = 0.0
var float prevPoc = na
var float prevVah = na
var float prevVal = na
var bool  prevBad = false      // CRX-018: previous profile has MISSING / partial / wrong-type candles -> its levels are not alertable
var bool  prevNew = false      // set on the bar a new previous profile appeared
var table tbl = na
var table wm = na
type Bud
    int res = 0
    int dBx = 0
    int dLn = 0
    int dLb = 0

var Bud bud = Bud.new()
var array<box> bxs = array.new<box>()
var array<line> lns = array.new<line>()
var array<label> lbs = array.new<label>()
var float devPoc = na
var float devVah = na
var float devVal = na
var array<Prof> slotCur = array.new<Prof>(4, na)
var array<Prof> slotA = array.new<Prof>(4, na)
var array<Prof> slotB = array.new<Prof>(4, na)
var array<bool> slotWas = array.new_bool(4, false)
var array<int> slotIdPrev = array.new_int(4, 0)
var array<float> slotRng = array.new_float(4, 0.0)
var array<int> slotS = array.new_int(4, 0)
var array<int> slotE = array.new_int(4, 0)
var int ancBar = na
var int ancSnapSec = 0   // CRX-016: seconds of the candle that contains the anchor which are NOT included (snap to the next bar open)
var bool ancBeforeHist = false
var int weekStartT = 0
var int monthStartT = 0
var array<string> rowK = array.new_string()
var array<string> rowV = array.new_string()
var array<color> rowC = array.new<color>()
var int tblRows = 0

// close the running profile: compute levels; the caller stores it (functions cannot modify globals)
finalizeProf(Prof p) =>
    bool ok = false
    if p.up.size() > 0 and p.total > 0
        setLevels(p)
        p.naked := true
        p.side := close[1] - p.poc >= 0 ? 1.0 : -1.0       // which side of the POC the profile closed on (used by the Close revisit rule)
        ok := true
    ok

if effMode != M_ROLL
    if (pEnd or pNew) and not na(cur)
        if finalizeProf(cur)
            profs.push(cur)
            if profs.size() > 40
                profs.shift()
            if not na(cur.hi) and not na(cur.lo) and cur.hi > cur.lo            // a profile with zero volume keeps hi/lo na: never feed na into the size estimate
                rngHist.push(cur.hi - cur.lo)
                if rngHist.size() > 5
                    rngHist.shift()
            prevPoc := cur.poc
            prevVah := cur.vah
            prevVal := cur.val
            prevBad := cur.qS > 0 or cur.qPart > 0 or cur.qTm > 0
            prevNew := true
        cur := na
        devPoc := na
        devVah := na
        devVal := na
    if na(cur) and pActive and (pNew or effMode != M_SESS or barstate.isfirst)
        float est = rngHist.size() > 0 ? rngHist.avg() : bootRng
        float rsNew = rowMode == 'Fixed ticks' ? rowTicks * syminfo.mintick : math.max(syminfo.mintick, math.round(est / rows / syminfo.mintick) * syminfo.mintick)
        rsNew := math.max(rsNew, math.ceil(est / 1500.0 / syminfo.mintick) * syminfo.mintick)    // safety cap: at most ~1500 rows per profile
        cur := newProf(bar_index, time, rsNew, 0.0)

    // revisit check for naked POCs (bars after the profile ended)
    if profs.size() > 0
        for i = 0 to profs.size() - 1
            Prof q = profs.get(i)
            if q.naked and bar_index > q.endBar and not na(q.poc)
                bool hit = false
                if barstate.isconfirmed
                    if nakedTouch == 'Wick'
                        hit := low <= q.poc and high >= q.poc
                    else
                        hit := (close - q.poc) * q.side <= 0
                if hit
                    q.naked := false
                    q.touchBar := bar_index
                    q.touchTime := time

    // add the bar that just closed to the running profile
    if barstate.isconfirmed and pActive and not na(cur)
        [cI, cF, cS, cvE, cvG, cvSp, cvPa, cvTm, cvI, cvC, accC] = feedBar(cur, effMode == M_SESS, sessS, sessE, sessTz)
        covExp += cvE
        covGot += cvG
        covSparse += cvSp
        covPartial += cvPa
        covTypeMis += cvTm
        covVolIn += cvI
        covVolChart += cvC
        ltfCnt += cI
        fbCnt += cF
        skipCnt += cS
        cur.qI += cI
        cur.qF += cF
        cur.qS += cS
        cur.qExp += cvE
        cur.qGot += cvG
        cur.qPart += cvPa
        cur.qTm += cvTm
        cur.qVolIn += cvI
        cur.qVolChart += cvC
        float prevCum = cur.cum.size() > 0 ? cur.cum.last() : 0.0
        cur.cum.push(prevCum + accC)
        cur.cumT.push(time - cur.startTime)
        // CRX-012: the IB window is [real period start, +ibMin). In Session mode the real start is the session's own start time (it can fall
        // inside a bar); bars fully inside use their H/L, a bar straddling the window edge uses only its intrabars inside the window, and is
        // left out (IB flagged approximate) when there are none. The bar BEFORE the first in-session bar is not part of this profile.
        int ibT0 = cur.startTime
        if effMode == M_SESS
            int offMin = (hour(cur.startTime, sessTz) * 60 + minute(cur.startTime, sessTz) - sessS + 1440) % 1440
            if offMin * 60 < chartSec
                ibT0 := cur.startTime - offMin * 60000
                if offMin > 0
                    cur.ibApprox := true
        int ibT1 = ibT0 + ibMin * 60000
        if time < ibT1
            if time + chartSec * 1000 <= ibT1
                cur.ibH := na(cur.ibH) ? high : math.max(cur.ibH, high)
                cur.ibL := na(cur.ibL) ? low : math.min(cur.ibL, low)
            else
                bool gotIb = false
                int ibN = ltfN()
                if ibN > 0
                    for k = 0 to ibN - 1
                        int tk = ltm.get(k)
                        if tk >= ibT0 and tk < ibT1 and (effMode != M_SESS or inSessAt(tk, sessS, sessE, sessTz))
                            cur.ibH := na(cur.ibH) ? lh.get(k) : math.max(cur.ibH, lh.get(k))
                            cur.ibL := na(cur.ibL) ? ll.get(k) : math.min(cur.ibL, ll.get(k))
                            gotIb := true
                if not gotIb
                    cur.ibApprox := true
        cur.endBar := bar_index
        cur.endTime := time
        if devOn and cur.up.size() > 0
            [pI, dlo, dhi] = calcVA(cur, vaPct, vaMode)
            if pI >= 0
                devPoc := cur.off + (cur.base + pI + 0.5) * cur.rs
                devVah := cur.off + (cur.base + dhi + 1) * cur.rs
                devVal := cur.off + (cur.base + dlo) * cur.rs

// ---- week / month starts (for the composite) and the anchor bar
if timeframe.change('W')
    weekStartT := time
if timeframe.change('M')
    monthStartT := time
if ancOn and na(ancBar) and ancTime > 0 and time >= ancTime
    ancBar := bar_index
    ancBeforeHist := bar_index == 0
    ancSnapSec := (bar_index > 0 and ancTime > time[1]) ? int((time - ancTime) / 1000) : 0

// ---- multi-session slots: up to four independent session profiles (latest finished + previous finished + running)
array<string> slotSessArr = array.from(ms1Sess, ms2Sess, ms3Sess, ms4Sess)
array<string> slotTzArr = array.from(ms1Tz, ms2Tz, ms3Tz, ms4Tz)
array<bool> slotOnArr = array.from(ms1On, ms2On, ms3On, ms4On)
if barstate.isfirst
    for s0 = 0 to 3
        string ss0 = slotSessArr.get(s0)
        slotS.set(s0, int(str.tonumber(str.substring(ss0, 0, 2))) * 60 + int(str.tonumber(str.substring(ss0, 2, 4))))
        slotE.set(s0, int(str.tonumber(str.substring(ss0, 5, 7))) * 60 + int(str.tonumber(str.substring(ss0, 7, 9))))
for s = 0 to 3
    if slotOnArr.get(s)
        bool inS = not na(time(timeframe.period, slotSessArr.get(s), slotTzArr.get(s)))
        bool was = slotWas.get(s)
        int idNow = inS ? sessIdAt(time, slotS.get(s), slotE.get(s), slotTzArr.get(s)) : 0
        bool idChg = inS and was and idNow != slotIdPrev.get(s)
        if was and (not inS or idChg)
            Prof fin = slotCur.get(s)
            if not na(fin)
                if finalizeProf(fin)
                    slotB.set(s, slotA.get(s))
                    slotA.set(s, fin)
                    if not na(fin.hi) and not na(fin.lo) and fin.hi > fin.lo
                        slotRng.set(s, fin.hi - fin.lo)
                slotCur.set(s, na)
        if inS and (not was or idChg)
            float estS = slotRng.get(s) > 0 ? slotRng.get(s) : bootRng
            float rsS = rowMode == 'Fixed ticks' ? rowTicks * syminfo.mintick : math.max(syminfo.mintick, math.round(estS / rows / syminfo.mintick) * syminfo.mintick)
            rsS := math.max(rsS, math.ceil(estS / 1500.0 / syminfo.mintick) * syminfo.mintick)
            slotCur.set(s, newProf(bar_index, time, rsS, 0.0))
        if inS and barstate.isconfirmed
            Prof sc = slotCur.get(s)
            if not na(sc)
                [sI, sF, sK, _a1, _a2, _a3, _a4, _a5, _a6, _a7, _a8] = feedBar(sc, true, slotS.get(s), slotE.get(s), slotTzArr.get(s))
                sc.endBar := bar_index
                sc.endTime := time
        slotWas.set(s, inS)
        slotIdPrev.set(s, idNow)

plot(devOn ? devPoc : na, 'Developing POC', color = devPocCol, linewidth = 2, style = plot.style_linebr)
pDV1 = plot(devOn ? devVah : na, 'Developing VAH', color = devVaCol, linewidth = 1, style = plot.style_linebr)
pDV2 = plot(devOn ? devVal : na, 'Developing VAL', color = devVaCol, linewidth = 1, style = plot.style_linebr)
fill(pDV1, pDV2, devFill ? color.new(devVaCol, 92) : na, 'Developing VA')

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// DRAWING (last bar only)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
rowTotal(Prof p, int i) =>
    p.up.get(i) + p.dn.get(i)

rowTop(Prof p, int i) =>
    p.off + (p.base + i + 1) * p.rs

rowBot(Prof p, int i) =>
    p.off + (p.base + i) * p.rs

// Drawing budget (CRX-020): every box/line/label goes through these helpers. bud.res is the number of
// slots (out of 500 per type) kept free for higher-priority items; anything that does not fit is counted in bud.d*
// Runtime fix: xloc.bar_index drawing objects cannot point more than a few thousand bars behind the current
// bar (a hard TradingView limit, independent of max_bars_back); a Day/Week/Month profile on an intraday chart
// easily exceeds that span. Every x1/x2/x here is therefore a real UNIX-ms TIME (xloc.bar_time), which has no
// such distance limit.
mkBox(int x1, float y1, int x2, float y2, color c) =>
    box b = na
    if bxs.size() < 500 - bud.res
        b := box.new(x1, y1, x2, y2, xloc = xloc.bar_time, border_color = na, bgcolor = c)
        bxs.push(b)
    else
        bud.dBx += 1
    b

mkLine(int x1, float y1, int x2, float y2, color c, int w, string sty) =>
    line l = na
    if lns.size() < 500 - bud.res
        l := line.new(x1, y1, x2, y2, xloc = xloc.bar_time, color = c, width = w, style = sty)
        lns.push(l)
    else
        bud.dLn += 1
    l

mkLabel(int x, float y, string t, color c) =>
    label lb = na
    if lbs.size() < 500 - bud.res
        lb := label.new(x, y, t, xloc = xloc.bar_time, style = label.style_label_left, color = color.new(c, 100), textcolor = c, size = lblSize)
        lbs.push(lb)
    else
        bud.dLb += 1
    lb

drawProf(Prof p, bool isCur, bool isLatestDone, bool withHist, int wBars, int nodeBudget) =>
    int n = p.up.size()
    if n > 0
        int barMs = chartSec * 1000     // 1 chart bar, in the same real-time units as every x-coordinate below (xloc.bar_time)
        int x0 = p.startTime
        int xRight = (isCur or (isLatestDone and extPrev)) ? time + lineExt * barMs : p.endTime
        if xRight <= x0
            xRight := x0 + barMs
        // Value Area shade
        if shadeVA and not na(p.vah)
            bud.res := 0
            mkBox(x0, p.vah, xRight, p.val, color.new(vaLineCol, 93))
        // histogram (current profile = essential; older ones leave 30 slots free)
        bud.res := isCur ? 0 : 30
        if withHist and showHist
            float mx = 0.0
            for i = 0 to n - 1
                mx := math.max(mx, rowTotal(p, i))
            [pocI, vaLo, vaHi] = calcVA(p, vaPct, vaMode)
            if mx > 0
                bool leftDir = histSide == 'Left → right'
                int anchor = leftDir ? x0 : (isCur ? time : p.endTime)
                for i = 0 to n - 1
                    float tot = rowTotal(p, i)
                    int len = math.round(tot / mx * wBars)
                    if len >= 1
                        bool inVA = i >= vaLo and i <= vaHi
                        float yT = rowTop(p, i)
                        float yB = rowBot(p, i)
                        int lenMs = len * barMs
                        if splitUD
                            int upLen = math.round(p.up.get(i) / mx * wBars)
                            upLen := math.min(upLen, len)
                            int upLenMs = upLen * barMs
                            int dnLenMs = lenMs - upLenMs
                            color cU = i == pocI ? pocRowCol : color.new(upCol, inVA ? inTr : outTr)
                            color cD = i == pocI ? pocRowCol : color.new(dnCol, inVA ? inTr : outTr)
                            if leftDir
                                if upLen > 0
                                    mkBox(anchor, yT, anchor + upLenMs, yB, cU)
                                if dnLenMs > 0
                                    mkBox(anchor + upLenMs, yT, anchor + lenMs, yB, cD)
                            else
                                if upLen > 0
                                    mkBox(anchor - upLenMs, yT, anchor, yB, cU)
                                if dnLenMs > 0
                                    mkBox(anchor - lenMs, yT, anchor - upLenMs, yB, cD)
                        else
                            color cS = i == pocI ? pocRowCol : color.new(inVA ? vaCol : outCol, inVA ? inTr : outTr)
                            if leftDir
                                mkBox(anchor, yT, anchor + lenMs, yB, cS)
                            else
                                mkBox(anchor - lenMs, yT, anchor, yB, cS)
        // levels (essential)
        bud.res := 0
        if showPoc and not na(p.poc)
            mkLine(x0, p.poc, xRight, p.poc, pocCol, pocW, line.style_solid)
        if showVA and not na(p.vah)
            mkLine(x0, p.vah, xRight, p.vah, vaLineCol, vaLineW, line.style_solid)
            mkLine(x0, p.val, xRight, p.val, vaLineCol, vaLineW, line.style_solid)
        if showLbl and (isCur or isLatestDone) and not na(p.poc)
            string tag = isCur ? '' : 'p'
            if showPoc
                mkLabel(xRight, p.poc, tag + 'POC ' + fmtPrice(p.poc), pocCol)
            if showVA
                mkLabel(xRight, p.vah, tag + 'VAH ' + fmtPrice(p.vah), vaLineCol)
                mkLabel(xRight, p.val, tag + 'VAL ' + fmtPrice(p.val), vaLineCol)
        // nodes (optional)
        bud.res := 60
        if nodesOn and nodeBudget > 0
            // same edge as the histogram anchor (bool leftDir above), so HVN/LVN markers stay on the side
            // that is guaranteed visible (near xRight / current time) even when x0 is far off-screen
            int nodeW = math.max(wBars, 8) * barMs
            int nx0 = histSide == 'Left → right' ? x0 : math.max(x0, xRight - nodeW)
            int nx1 = histSide == 'Left → right' ? math.min(xRight, x0 + nodeW) : xRight
            if nodeMode == 'Advanced'
                Nodes nd = advNodes(p)
                if nd.hI.size() > 0
                    for k = 0 to nd.hI.size() - 1
                        int ri = nd.hI.get(k)
                        float yy = p.off + (p.base + ri + 0.5) * p.rs
                        if nodeZones
                            mkBox(nx0, rowTop(p, nd.hZh.get(k)), nx1, rowBot(p, nd.hZl.get(k)), color.new(hvnCol, 86))
                        mkLine(nx0, yy, nx1, yy, hvnCol, 1, line.style_dashed)
                        mkLabel(nx1, yy, 'HVN', hvnCol)
                if nd.lI.size() > 0
                    for k = 0 to nd.lI.size() - 1
                        int ri = nd.lI.get(k)
                        float yy = p.off + (p.base + ri + 0.5) * p.rs
                        if nodeZones
                            mkBox(nx0, rowTop(p, nd.lZh.get(k)), nx1, rowBot(p, nd.lZl.get(k)), color.new(lvnCol, 88))
                        mkLine(nx0, yy, nx1, yy, lvnCol, 1, line.style_dotted)
                        mkLabel(nx1, yy, 'LVN', lvnCol)
            else
                [hv, lv] = findNodes(p)
                if hv.size() > 0
                    for k = 0 to hv.size() - 1
                        int ri = hv.get(k)
                        float yy = p.off + (p.base + ri + 0.5) * p.rs
                        mkLine(nx0, yy, nx1, yy, hvnCol, 1, line.style_dashed)
                        mkLabel(nx1, yy, 'HVN', hvnCol)
                if lv.size() > 0
                    for k = 0 to lv.size() - 1
                        int ri = lv.get(k)
                        float yy = p.off + (p.base + ri + 0.5) * p.rs
                        mkLine(nx0, yy, nx1, yy, lvnCol, 1, line.style_dotted)
                        mkLabel(nx1, yy, 'LVN', lvnCol)

// composite / anchored / session profile: POC, VAH, VAL lines (+ optional single-colour histogram)
// xs: the left edge, as a TIME (xloc.bar_time) - callers pass p.startTime (see below)
drawSpecial(Prof p, color c, string tag, bool hist, bool fromRight, int wBars, int xs) =>
    if not na(p) and not na(p.poc)
        int n = p.up.size()
        int barMs = chartSec * 1000
        int xR = time + lineExt * barMs
        int xL = xs
        if xR <= xL
            xR := xL + barMs
        if hist and n > 0
            float mx = 0.0
            for i = 0 to n - 1
                mx := math.max(mx, rowTotal(p, i))
            [pocI, vaLo, vaHi] = calcVA(p, vaPct, vaMode)
            if mx > 0
                for i = 0 to n - 1
                    int lenMs = math.round(rowTotal(p, i) / mx * wBars) * barMs
                    if lenMs >= barMs
                        bool inVA = i >= vaLo and i <= vaHi
                        color cc = i == pocI ? c : color.new(c, inVA ? 55 : 80)
                        if fromRight
                            mkBox(xR - lenMs, rowTop(p, i), xR, rowBot(p, i), cc)
                        else
                            mkBox(xL, rowTop(p, i), xL + lenMs, rowBot(p, i), cc)
        mkLine(xL, p.poc, xR, p.poc, c, 2, line.style_solid)
        mkLine(xL, p.vah, xR, p.vah, c, 1, line.style_dashed)
        mkLine(xL, p.val, xR, p.val, c, 1, line.style_dashed)
        mkLabel(xR, p.poc, tag + ' POC ' + fmtPrice(p.poc), c)
        mkLabel(xR, p.vah, tag + ' VAH ' + fmtPrice(p.vah), c)
        mkLabel(xR, p.val, tag + ' VAL ' + fmtPrice(p.val), c)

drawIB(Prof p) =>
    if not na(p) and not na(p.ibH) and not na(p.ibL)
        int xR = time + lineExt * chartSec * 1000
        float h = p.ibH
        float l = p.ibL
        float r = h - l
        mkLine(p.startTime, h, xR, h, ibCol, 2, line.style_solid)
        mkLine(p.startTime, l, xR, l, ibCol, 2, line.style_solid)
        mkLabel(xR, h, 'IBH ' + fmtPrice(h), ibCol)
        mkLabel(xR, l, 'IBL ' + fmtPrice(l), ibCol)
        if ibExt and r > 0
            for mlt = 1 to 2
                mkLine(p.startTime, h + mlt * r, xR, h + mlt * r, ibCol, 1, line.style_dotted)
                mkLine(p.startTime, l - mlt * r, xR, l - mlt * r, ibCol, 1, line.style_dotted)
                mkLabel(xR, h + mlt * r, 'IB+' + str.tostring(mlt) + 'x', ibCol)
                mkLabel(xR, l - mlt * r, 'IB-' + str.tostring(mlt) + 'x', ibCol)

addRow(string k, string v, color c) =>
    rowK.push(k)
    rowV.push(v)
    rowC.push(c)

clearAll() =>
    if bxs.size() > 0
        for i = 0 to bxs.size() - 1
            box.delete(bxs.get(i))
        bxs.clear()
    if lns.size() > 0
        for i = 0 to lns.size() - 1
            line.delete(lns.get(i))
        lns.clear()
    if lbs.size() > 0
        for i = 0 to lbs.size() - 1
            label.delete(lbs.get(i))
        lbs.clear()

if barstate.islast
    clearAll()
    rowK.clear()
    rowV.clear()
    rowC.clear()
    // rolling profile is built from chart bars (no intrabar history is stored for it)
    if effMode == M_ROLL
        int ro = rollLive or barstate.isconfirmed ? 0 : 1
        float hhR = na
        float llR = na
        for k = ro to ro + rollN - 1
            if bar_index - k >= 0
                hhR := na(hhR) ? high[k] : math.max(hhR, high[k])
                llR := na(llR) ? low[k] : math.min(llR, low[k])
        float rsR = math.max(syminfo.mintick, (nz(hhR, high) - nz(llR, low)) / rows)
        Prof q = newProf(math.max(0, bar_index - ro - rollN + 1), time[math.min(rollN - 1 + ro, bar_index)], rsR, nz(llR, low))
        for k = ro + rollN - 1 to ro
            if bar_index - k >= 0
                addVol(q, high[k], low[k], volume[k], close[k] >= open[k])
        q.endBar := bar_index - ro
        q.endTime := time[ro]
        if q.up.size() > 0
            setLevels(q)
        cur := q
    int span = profs.size() > 0 ? profs.last().endBar - profs.last().startBar + 1 : (na(cur) ? 20 : bar_index - cur.startBar + 1)
    if effMode == M_ROLL
        span := rollN
    else if not na(cur)
        span := math.max(span, bar_index - cur.startBar + 1)
    int wBars = math.max(3, math.round(span * widthPct / 100.0))
    bud.res := 0
    bud.dBx := 0
    bud.dLn := 0
    bud.dLb := 0
    int budget = 440
    int perRow = splitUD ? 2 : 1
    int nodeLeft = nodeProfs
    bool hasCurNow = not na(cur) and cur.up.size() > 0
    // newest first: current profile, then finished profiles
    if hasCurNow
        setLevels(cur)
        bool ok = cur.up.size() * perRow + 3 <= budget
        drawProf(cur, true, false, ok, wBars, nodeLeft)
        budget -= (ok ? cur.up.size() * perRow : 0) + 3
        nodeLeft -= 1
    if effMode != M_ROLL and profs.size() > 0
        int lastIdx = profs.size() - 1
        int firstIdx = math.max(0, lastIdx - keepPrev + 1)
        if keepPrev > 0
            for ii = lastIdx to firstIdx
                Prof pp = profs.get(ii)
                bool ok = pp.up.size() * perRow + 3 <= budget
                drawProf(pp, false, ii == lastIdx, ok, wBars, nodeLeft)
                budget -= (ok ? pp.up.size() * perRow : 0) + 3
                nodeLeft -= 1
        // naked POCs (priority below levels/histogram): active ones with age / distance, plus the last few consumed ones (faded)
        if nakedOn
            bud.res := 30
            int shownN = 0
            int shownC = 0
            for ii = lastIdx to 0
                Prof pn = profs.get(ii)
                if not na(pn.poc)
                    int age = bar_index - pn.endBar
                    if pn.naked
                        if shownN < nakedMax and (nakedMaxAge <= 0 or age <= nakedMaxAge)
                            float dist = (pn.poc - close) / close * 100.0
                            mkLine(pn.endTime, pn.poc, time + lineExt * chartSec * 1000, pn.poc, nakedCol, 1, line.style_dashed)
                            mkLabel(time + lineExt * chartSec * 1000, pn.poc, 'nPOC ' + str.format_time(pn.startTime, 'dd MMM', syminfo.timezone) + ' ' + fmtPrice(pn.poc) + ' · ' + str.tostring(age) + ' bars · ' + str.tostring(dist, '#.##') + '%' + (nakedProv and nakedTouch == 'Wick' and not barstate.isconfirmed and low <= pn.poc and high >= pn.poc ? ' · provisional touch' : ''), nakedCol)
                            shownN += 1
                    else if shownC < nakedConsumed and not na(pn.touchBar)
                        mkLine(pn.endTime, pn.poc, pn.touchTime, pn.poc, color.new(nakedCol, 65), 1, line.style_dotted)
                        mkLabel(pn.touchTime, pn.poc, 'touched ' + str.format_time(pn.startTime, 'dd MMM', syminfo.timezone) + ' ' + fmtPrice(pn.poc), color.new(nakedCol, 65))
                        shownC += 1
    // optional layers below keep 60 slots free for essentials
    bud.res := 60
    // Initial Balance of the profile period
    Prof refIB = hasCurNow ? cur : (profs.size() > 0 ? profs.last() : na)
    if ibOn and effMode != M_ROLL
        drawIB(refIB)
    // composite of finished profiles (+ the running one)
    Prof comp = na
    int compSrcN = 0
    if compOn and effMode != M_ROLL
        array<Prof> srcs = array.new<Prof>()
        int lim = compMode == 'Last N profiles' ? compN : 1000
        int startT = compMode == 'Current week' ? weekStartT : monthStartT
        int cntS = 0
        if hasCurNow
            if compMode == 'Last N profiles' or cur.startTime >= startT
                srcs.push(cur)
                cntS += 1
        if profs.size() > 0
            for ii = profs.size() - 1 to 0
                Prof pc = profs.get(ii)
                if cntS < lim
                    if compMode == 'Last N profiles' or pc.startTime >= startT
                        srcs.push(pc)
                        cntS += 1
        if srcs.size() > 0
            comp := makeComposite(srcs, compRows)
            compSrcN := srcs.size()
            drawSpecial(comp, compCol, 'Comp', compHist, true, math.max(8, wBars), comp.startTime)
    // anchored profile from chart bars
    Prof anc = na
    if ancOn and not na(ancBar)
        int nA = math.min(bar_index - ancBar + 1, 1900)
        float hA = high
        float lA = low
        for k = nA - 1 to 0
            hA := math.max(hA, high[k])
            lA := math.min(lA, low[k])
        float rsA = math.max(syminfo.mintick, math.round((hA - lA) / ancRows / syminfo.mintick) * syminfo.mintick)   // CRX-014: whole multiple of the tick
        anc := newProf(bar_index - nA + 1, time[nA - 1], rsA, lA)
        for k = nA - 1 to 0
            addVol(anc, high[k], low[k], volume[k], close[k] >= open[k])
        anc.endBar := bar_index
        anc.endTime := time
        if anc.up.size() > 0
            setLevels(anc)
            drawSpecial(anc, ancCol, 'Anch', ancHist, false, math.max(8, wBars), anc.startTime)
    // session profiles
    for s = 0 to 3
        if slotOnArr.get(s)
            color sc = s == 0 ? ms1Col : s == 1 ? ms2Col : s == 2 ? ms3Col : ms4Col
            string sn = s == 0 ? 'Asia' : s == 1 ? 'London' : s == 2 ? 'NY' : 'Custom'
            Prof sB = slotB.get(s)
            Prof sA = slotCur.get(s)
            bool running = not na(sA) and sA.up.size() > 0
            if not running
                sA := slotA.get(s)
            if not na(sB)
                drawSpecial(sB, color.new(sc, 40), 'p' + sn, false, false, 8, sB.startTime)
            if not na(sA) and sA.up.size() > 0
                setLevels(sA)
                drawSpecial(sA, sc, sn, msHist, false, math.max(8, wBars), sA.startTime)
    // confluence zones
    int confN = 0
    if confOn
        array<float> lvP = array.new_float()
        array<string> lvN = array.new_string()
        array<string> lvK = array.new_string()
        Prof dnP = profs.size() > 0 ? profs.last() : na
        if not na(dnP)
            lvP.push(dnP.poc)
            lvN.push('pPOC')
            lvK.push('P' + str.tostring(profs.size() - 1))
            lvP.push(dnP.vah)
            lvN.push('pVAH')
            lvK.push('P' + str.tostring(profs.size() - 1))
            lvP.push(dnP.val)
            lvN.push('pVAL')
            lvK.push('P' + str.tostring(profs.size() - 1))
        if not na(comp)
            lvP.push(comp.poc)
            lvN.push('cPOC')
            lvK.push('C')
            lvP.push(comp.vah)
            lvN.push('cVAH')
            lvK.push('C')
            lvP.push(comp.val)
            lvN.push('cVAL')
            lvK.push('C')
        if not na(anc) and not na(anc.poc)
            lvP.push(anc.poc)
            lvN.push('aPOC')
            lvK.push('A')
        if ibOn and not na(refIB) and not na(refIB.ibH)
            lvP.push(refIB.ibH)
            lvN.push('IBH')
            lvK.push('P' + (hasCurNow ? 'cur' : str.tostring(profs.size() - 1)))
            lvP.push(refIB.ibL)
            lvN.push('IBL')
            lvK.push('P' + (hasCurNow ? 'cur' : str.tostring(profs.size() - 1)))
        if profs.size() > 0
            for ii = profs.size() - 1 to 0
                Prof pq = profs.get(ii)
                if pq.naked and not na(pq.poc) and (nakedMaxAge <= 0 or bar_index - pq.endBar <= nakedMaxAge)
                    lvP.push(pq.poc)
                    lvN.push('nPOC')
                    lvK.push('P' + str.tostring(ii))
        if nodesOn
            for z = 0 to 1
                Prof hp = z == 0 ? cur : dnP
                if not na(hp) and hp.up.size() > 0
                    Nodes hn = advNodes(hp)
                    if hn.hI.size() > 0
                        for q2 = 0 to hn.hI.size() - 1
                            lvP.push(hp.off + (hp.base + hn.hI.get(q2) + 0.5) * hp.rs)
                            lvN.push('HVN')
                            lvK.push('P' + (z == 0 ? 'cur' : str.tostring(profs.size() - 1)))
        float tolC = confTol * atr14
        if tolC > 0
            Zones zs = clusterLevels(lvP, lvN, lvK, tolC, confMin)
            confN := zs.lo.size()
            if confN > 0
                array<float> dists = array.new_float()
                for z = 0 to confN - 1
                    dists.push(math.abs((zs.lo.get(z) + zs.hi.get(z)) / 2 - close))
                array<int> ord = array.sort_indices(dists, order.ascending)
                for z = 0 to math.min(confN, confMax) - 1
                    int zi = ord.get(z)
                    float padC = tolC * 0.25
                    bud.res := 60
                    if true
                        mkBox(time - 80 * chartSec * 1000, zs.hi.get(zi) + padC, time + lineExt * chartSec * 1000, zs.lo.get(zi) - padC, color.new(confCol, 78))
                    mkLabel(time + lineExt * chartSec * 1000, (zs.lo.get(zi) + zs.hi.get(zi)) / 2, 'Confluence x' + str.tostring(zs.n.get(zi)) + ' ' + zs.s.get(zi), confCol)

    // ---- rows of the status table
    Prof A = hasCurNow ? cur : (profs.size() > 0 ? profs.last() : na)
    Prof B = na
    if not na(A)
        if hasCurNow
            B := profs.size() > 0 ? profs.last() : na
        else
            B := profs.size() > 1 ? profs.get(profs.size() - 2) : na

    color tBase = color.new(color.gray, 0)
    bool hasCur = hasCurNow and cur.total > 0
    float cPoc = hasCurNow ? cur.poc : na
    string vtype = syminfo.volumetype
    string vtxt = vtype == 'tick' ? 'Tick volume (approx.)' : vtype == 'base' ? 'Real · base' : vtype == 'quote' ? 'Real · quote' : 'Real / n.a.'
    color vcol = vtype == 'tick' ? color.orange : color.green
    string srcTxt = effMode == M_ROLL ? 'Chart bars' : (useLtf ? ltfStr + 'm intrabars' : 'Chart bars')
    int cov = ltfCnt + fbCnt + skipCnt
    string covTxt = effMode == M_ROLL or not useLtf ? '—' : (cov > 0 ? str.tostring(ltfCnt * 100.0 / cov, '#') + '% intrabar' + (fbCnt > 0 ? ' · ' + str.tostring(fbCnt * 100.0 / cov, '#') + '% chart-bar' : '') + (skipCnt > 0 ? ' · ' + str.tostring(skipCnt * 100.0 / cov, '#') + '% missing' : '') + (covExp > 0 ? ' · time cov ' + str.tostring(covGot * 100.0 / covExp, '#.0') + '%' + (covVolChart > 0 ? ' · vol cov ' + str.tostring(covVolIn * 100.0 / covVolChart, '#.0') + '%' : '') : '') + (covSparse > 0 ? ' · ' + str.tostring(covSparse) + ' sparse (no-trade minutes)' : '') + (covPartial > 0 ? ' · ' + str.tostring(covPartial) + ' PARTIAL (Strict, volume mismatch)' : '') + (covTypeMis > 0 ? ' · ' + str.tostring(covTypeMis) + ' volume-type mismatch' : '') : '—')
    float vaCovP = na
    if hasCur
        [cpI, cLo, cHi] = calcVA(cur, vaPct, vaMode)
        if cpI >= 0
            float vs = 0.0
            for r = cLo to cHi
                vs += cur.up.get(r) + cur.dn.get(r)
            vaCovP := vs / cur.total * 100.0
    string vaTxt = (vaMode == 0 ? 'TV rule' : vaMode == 1 ? 'Target 1-row' : 'Pair 2-row') + ' · ' + str.tostring(vaPct, '#') + '% → covers ' + (na(vaCovP) ? '—' : str.tostring(vaCovP, '#.#') + '%')
    string where = '—'
    color whereCol = tBase
    if hasCur and not na(cur.vah)
        where := close > cur.vah ? 'Above VA' : close < cur.val ? 'Below VA' : 'Inside VA'
        whereCol := close > cur.vah ? color.green : close < cur.val ? color.red : color.gray
    string dist = '—'
    if hasCur and not na(cPoc)
        dist := str.tostring((close - cPoc) / cPoc * 100, '#.##') + '% from POC'
    string dltTxt = '—'
    color dltCol = tBase
    if hasCur
        float upP = cur.upTotal / cur.total * 100
        dltTxt := 'Up ' + str.tostring(upP, '#') + '% / Down ' + str.tostring(100 - upP, '#') + '%'
        dltCol := upP >= 52 ? color.green : upP <= 48 ? color.red : color.gray
    int nakedCnt = 0
    float nearNaked = na
    if profs.size() > 0
        for i = 0 to profs.size() - 1
            Prof qn = profs.get(i)
            if qn.naked and not na(qn.poc) and (nakedMaxAge <= 0 or bar_index - qn.endBar <= nakedMaxAge)
                nakedCnt += 1
                if na(nearNaked) or math.abs(qn.poc - close) < math.abs(nearNaked - close)
                    nearNaked := qn.poc
    string nkTxt = nakedCnt == 0 ? 'None' : str.tostring(nakedCnt) + ' · nearest ' + fmtPrice(nearNaked)
    string warn = ''
    if effMode != M_ROLL and effMode != M_SESS and ((effMode == M_DAY and chartSec >= 86400) or (effMode == M_WEEK and chartSec >= 604800))
        warn := 'Chart TF ≥ period'
    else if hasCur and cur.total <= 0
        warn := 'No volume data'
    else if useLtf and effMode != M_ROLL and cov > 0 and ltfCnt == 0
        warn := volSrc == 'Strict intrabars' ? 'Strict: no intrabar data - candles are MISSING (chart bars not used)' : 'No intrabar data: chart-bar volume'

    addRow('Volume type', vtxt, vcol)
    addRow('Source', srcTxt + (covTxt != '—' ? ' · ' + covTxt : ''), tBase)
    addRow('Period', (mode == M_AUTO ? 'Auto → ' : '') + (effMode == M_ROLL ? 'Last ' + str.tostring(rollN) + (rollLive ? ' bars (live)' : ' closed bars') : effMode), tBase)
    addRow('Rows', hasCur ? str.tostring(cur.up.size()) + ' × ' + fmtPrice(cur.rs) : '—', tBase)
    if effMode != M_ROLL and useLtf and hasCur
        addRow('Data · current', qualTxt(cur), qualPoor(cur) ? color.orange : color.green)
    if effMode != M_ROLL and useLtf and profs.size() > 0
        addRow('Data · previous', qualTxt(profs.last()), qualPoor(profs.last()) ? color.orange : color.green)
    if bud.dBx + bud.dLn + bud.dLb > 0
        addRow('Drawings', 'limit reached - dropped ' + str.tostring(bud.dBx) + ' boxes, ' + str.tostring(bud.dLn) + ' lines, ' + str.tostring(bud.dLb) + ' labels (lowest priority first)', color.orange)
    addRow('POC', fmtPrice(cPoc), pocCol)
    addRow('VAH / VAL', hasCur ? fmtPrice(cur.vah) + ' / ' + fmtPrice(cur.val) : '—', vaLineCol)
    addRow('Price', where + ' · ' + dist, whereCol)
    addRow('Est. up/down vol', dltTxt, dltCol)
    if not hasCurNow and not na(A)
        addRow('Current', 'waiting for the first closed bar', color.orange)
    // analytics against the previous profile
    if effMode != M_ROLL and not na(A) and not na(B) and not na(A.poc) and not na(B.poc)
        if migOn
            float dM = A.poc - B.poc
            string stM = migState(dM, A.rs)
            addRow('POC migration', (dM >= 0 ? '+' : '') + str.tostring(dM / syminfo.mintick, '#') + ' ticks (' + (dM >= 0 ? '+' : '') + str.tostring(dM / B.poc * 100, '#.##') + '%) · ' + stM, stM == 'Stable' ? color.gray : dM > 0 ? color.green : color.red)
        if vaShiftOn
            string vsT = vaShiftText(A.val, A.vah, B.val, B.vah)
            addRow('VA vs previous', vsT, vsT == 'Higher value' ? color.green : vsT == 'Lower value' ? color.red : color.white)
    if migOn and devOn and hasCurNow and not na(devPoc) and not na(devPoc[1])
        addRow('Dev POC last bar', (devPoc - devPoc[1] >= 0 ? '+' : '') + str.tostring((devPoc - devPoc[1]) / syminfo.mintick, '#') + ' ticks', devPoc > devPoc[1] ? color.green : devPoc < devPoc[1] ? color.red : color.gray)
    if shapeOn and not na(A) and A.up.size() > 0
        [spI, spa, spb] = calcVA(A, vaPct, vaMode)
        addRow('Shape (heuristic)', shapeOf(A, spI), color.white)
    if rvolOn and hasCurNow
        [rvV, rvU] = rvolOf(cur, profs)
        addRow('Rel. volume', rvV < 0 ? 'n/a (needs 2+ previous)' : str.tostring(rvV, '#.##') + 'x · avg of ' + str.tostring(rvU), rvV >= 1.5 ? color.green : rvV < 0 ? color.gray : rvV < 0.7 ? color.orange : color.white)
    if ibOn and not na(A) and not na(A.ibH)
        float rIB = A.ibH - A.ibL
        addRow('IB high / low', fmtPrice(A.ibH) + ' / ' + fmtPrice(A.ibL) + (A.ibApprox ? ' (approx.: partial bar without intrabars left out)' : ''), ibCol)
        string ibs = 'Inside IB'
        if rIB > 0 and close > A.ibH
            ibs := 'Above IB +' + str.tostring((close - A.ibH) / rIB, '#.##') + 'x'
        else if rIB > 0 and close < A.ibL
            ibs := 'Below IB -' + str.tostring((A.ibL - close) / rIB, '#.##') + 'x'
        addRow('IB range', fmtPrice(rIB) + ' · ' + ibs, color.white)
    if not na(comp) and not na(comp.poc)
        addRow('Composite', 'POC ' + fmtPrice(comp.poc) + ' · VA ' + fmtPrice(comp.val) + '-' + fmtPrice(comp.vah) + ' · ' + str.tostring(compSrcN) + ' profiles · ' + vtxt, compCol)
        if effMode != M_ROLL and useLtf
            addRow('Data · composite', qualTxt(comp), qualPoor(comp) ? color.orange : color.green)
    else if compOn and mode != M_ROLL and ((compMode == 'Current week' and mode == M_MONTH) or compMode != 'Last N profiles')
        // CRX-015: only profiles that START inside the window are summed; a coarser primary period cannot be cut apart
        addRow('Composite', compMode == 'Current week' and mode == M_MONTH ? 'NOT BUILT: Current week needs primary period Week or shorter' : 'none yet (profiles starting before the window are left out)', color.orange)
    if not na(anc) and not na(anc.poc)
        // CRX-016: Pine builds the anchored profile from CHART bars (no intrabars) starting at the first bar that opens at/after the anchor
        string ancNote = ancSnapSec > 0 ? ' · SNAPPED to next bar open (' + str.tostring(math.round(ancSnapSec / 60.0)) + ' min of the anchor candle not included)' : ''
        ancNote += ancBeforeHist ? ' · anchor is before the loaded history: starts at the first bar' : ''
        ancNote += (bar_index - ancBar + 1) > 1900 ? ' · LIMITED to the last 1900 bars (Pine history limit): earlier part of the range not included' : ''
        addRow('Anchored', 'POC ' + fmtPrice(anc.poc) + ' · VA ' + fmtPrice(anc.val) + '-' + fmtPrice(anc.vah) + ancNote, ancSnapSec > 0 or ancBeforeHist ? color.orange : ancCol)
    for s = 0 to 3
        if slotOnArr.get(s)
            Prof sx = slotCur.get(s)
            bool runningX = not na(sx) and sx.up.size() > 0
            if not runningX
                sx := slotA.get(s)
            if not na(sx) and not na(sx.poc)
                string snx = s == 0 ? 'Asia' : s == 1 ? 'London' : s == 2 ? 'NY' : 'Custom'
                addRow(snx + (runningX ? '' : ' (done)'), 'POC ' + fmtPrice(sx.poc) + ' · H/L ' + fmtPrice(sx.hi) + '/' + fmtPrice(sx.lo) + ' · vol ' + str.tostring(sx.total, '#'), s == 0 ? ms1Col : s == 1 ? ms2Col : s == 2 ? ms3Col : ms4Col)
    if confOn
        addRow('Confluence', str.tostring(confN) + ' zone(s)', confCol)
    addRow('Prev POC', fmtPrice(prevPoc), pocCol)
    addRow('Prev VAH / VAL', fmtPrice(prevVah) + ' / ' + fmtPrice(prevVal), vaLineCol)
    addRow('Naked POCs', nkTxt, nakedCol)
    addRow('VA method', vaTxt, tBase)
    addRow('Note', warn != '' ? warn : (vtype == 'tick' ? 'Tick volume ≠ real turnover' : 'OK'), warn != '' ? color.red : (vtype == 'tick' ? color.orange : color.green))

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ALERTS (levels of the latest FINISHED profile)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// CRX-018: two families per level. "Closed ..." fire only on a CONFIRMED candle close (barstate.isconfirmed); "Live cross ..." fire intrabar when
// price crosses the level (may fire and un-fire while the candle is open). Nothing fires while the previous profile's data is unusable.
bool pOK = not na(prevVah) and not na(prevVal) and not prevBad
bool lvOK = not prevBad and not prevNew            // CRX-029: on the bar the reference levels themselves change, crossover(close, prevX) would compare against a moved level -> never alert
bool inPrevVA = pOK and close <= prevVah and close >= prevVal
// CW10002: ta.crossover/crossunder must run on every bar (unconditionally) to keep their internal history correct;
// compute each pair once here and reuse the plain boolean inside the conditional expressions below.
bool xUpPoc = ta.crossover(close, prevPoc)
bool xDnPoc = ta.crossunder(close, prevPoc)
bool xUpVah = ta.crossover(close, prevVah)
bool xDnVal = ta.crossunder(close, prevVal)
bool cf = barstate.isconfirmed and lvOK
alertcondition(cf and xUpPoc, 'Closed above previous POC', 'Crypex VP: candle CLOSED above the previous profile POC')
alertcondition(cf and xDnPoc, 'Closed below previous POC', 'Crypex VP: candle CLOSED below the previous profile POC')
alertcondition(cf and xUpVah, 'Closed above previous VAH', 'Crypex VP: candle CLOSED above the previous Value Area High')
alertcondition(cf and xDnVal, 'Closed below previous VAL', 'Crypex VP: candle CLOSED below the previous Value Area Low')
alertcondition(lvOK and xUpPoc, 'Live cross above previous POC', 'Crypex VP: price crossed above the previous profile POC (live, candle may not be closed)')
alertcondition(lvOK and xDnPoc, 'Live cross below previous POC', 'Crypex VP: price crossed below the previous profile POC (live, candle may not be closed)')
alertcondition(lvOK and xUpVah, 'Live cross above previous VAH', 'Crypex VP: price crossed above the previous Value Area High (live, candle may not be closed)')
alertcondition(lvOK and xDnVal, 'Live cross below previous VAL', 'Crypex VP: price crossed below the previous Value Area Low (live, candle may not be closed)')
alertcondition(cf and pOK and inPrevVA and not inPrevVA[1] and close[1] > nz(prevVah, close[1]), 'Re-entered previous VA from above', 'Crypex VP: candle closed back inside the previous Value Area from above')
alertcondition(cf and pOK and inPrevVA and not inPrevVA[1] and close[1] < nz(prevVal, close[1]), 'Re-entered previous VA from below', 'Crypex VP: candle closed back inside the previous Value Area from below')
alertcondition(barstate.isconfirmed and prevNew and not na(prevPoc), 'New period: previous levels updated', 'Crypex VP: a new profile period started - previous POC / VAH / VAL changed')
alertcondition(barstate.isconfirmed and prevNew and prevBad, 'Previous profile data incomplete', 'Crypex VP: the finished profile has missing or unusable intrabar data - its level alerts are suppressed')
prevNew := false

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// STATUS TABLE
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
if showTable and barstate.islast
    int nR = rowK.size() + 1
    if tblRows != nR
        if not na(tbl)
            table.delete(tbl)
        tbl := table.new(tablePos, 2, nR, border_width = 1, frame_color = color.new(color.gray, 40), border_color = color.new(color.gray, 55))
        tblRows := nR
    color hdr = color.new(BRAND_GREEN, 25)
    color bg0 = color.new(color.gray, 88)
    color bg1 = color.new(color.gray, 94)
    table.cell(tbl, 0, 0, 'Crypex VP', text_color = color.white, bgcolor = hdr, text_size = tableSize)
    table.cell(tbl, 1, 0, syminfo.ticker, text_color = color.white, bgcolor = hdr, text_size = tableSize)
    if rowK.size() > 0
        for i = 0 to rowK.size() - 1
            color bgr = i % 2 == 0 ? bg0 : bg1
            table.cell(tbl, 0, i + 1, rowK.get(i), text_color = color.gray, bgcolor = bgr, text_size = tableSize)
            table.cell(tbl, 1, i + 1, rowV.get(i), text_color = rowC.get(i), bgcolor = bgr, text_size = tableSize)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// WATERMARK
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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)
volume-profile-crypex.mq5 · MQL5 · ۲٬۶۰۲ خط
//+------------------------------------------------------------------+
//| Crypex | Volume Profile (MT5)                                    |
//| POC / Value Area / HVN-LVN / naked POC with intrabar volume     |
//| © 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 description "Volume Profile: POC, Value Area, HVN/LVN, naked POC, developing lines, status panel"
#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots   3

#property indicator_label1  "Developing POC"
#property indicator_type1   DRAW_LINE
#property indicator_color1  C'255,152,0'
#property indicator_width1  2
#property indicator_label2  "Developing VAH"
#property indicator_type2   DRAW_LINE
#property indicator_color2  C'41,98,255'
#property indicator_label3  "Developing VAL"
#property indicator_type3   DRAW_LINE
#property indicator_color3  C'41,98,255'

enum ENUM_VP_PERIOD { VP_AUTO, VP_DAY, VP_WEEK, VP_MONTH, VP_SESSION, VP_ROLL };
enum ENUM_VP_VA     { VA_TV, VA_TARGET1, VA_PAIR2 };
enum ENUM_VP_VSRC   { SRC_FALLBACK, SRC_STRICT, SRC_CHART };
enum ENUM_VP_SESSBND { SESSB_EXCLUDE, SESSB_ESTIMATE };
enum ENUM_VP_VTYPE  { VT_AUTO, VT_REAL, VT_TICK };
enum ENUM_VP_TZ     { TZ_SERVER, TZ_UTC, TZ_LONDON, TZ_NEWYORK, TZ_TOKYO, TZ_FIXED };
enum ENUM_VP_DST    { DST_NONE, DST_US, DST_EU, DST_MANUAL };
enum ENUM_VP_WEEK   { WK_AUTO, WK_MONDAY, WK_SUNDAY };
enum ENUM_VP_NODEMODE { NODE_ADV, NODE_SIMPLE };
enum ENUM_VP_TOUCH    { NT_WICK, NT_CLOSE };
enum ENUM_VP_COMP     { COMP_LASTN, COMP_WEEK, COMP_MONTH };
enum ENUM_VP_ALERT  { AL_CLOSE, AL_LIVE };
enum ENUM_VP_ROWS   { ROWS_AUTO, ROWS_FIXED, ROWS_COUNT };
enum ENUM_VP_DIR    { DIR_LEFT_RIGHT, DIR_RIGHT_LEFT };
enum ENUM_PANEL_POS { POS_TOP_RIGHT, POS_TOP_LEFT, POS_BOTTOM_RIGHT, POS_BOTTOM_LEFT };

input group "Profile"
input ENUM_VP_PERIOD InpPeriod   = VP_AUTO;        // Profile period (Auto picks Day/Week/Month/Last N bars by chart TF; or Day/Week/Month/Session/Last N bars)
input string InpSessStart  = "08:00";              // Session start (in the chosen time basis, Session mode)
input string InpSessEnd    = "16:00";              // Session end (in the chosen time basis, Session mode)
input ENUM_VP_TZ InpTz     = TZ_SERVER;            // Time basis for Day / Week / Month / Session boundaries
input double InpServerOffset = 2.0;                // Broker server UTC offset in WINTER (hours), used when the basis is not Server
input ENUM_VP_DST InpServerDst = DST_US;           // Broker server DST schedule (US = most brokers, EU, none, or the manual schedule below)
input string InpOffsetSched = "";                  // Manual schedule: "2025.03.30 01:00=3;2025.10.26 01:00=2" = from that UTC instant the server is at UTC+offset
input double InpFixedOffset = 0.0;                 // UTC offset (hours) for the "Fixed" time basis
input ENUM_VP_WEEK InpWeekStart = WK_AUTO;         // Week start (Auto: Monday on 24/7 symbols, Sunday otherwise)
input int    InpRollN      = 300;                  // N bars (Last N bars mode, chart-bar data)
input int    InpKeepPrev   = 2;                    // Previous profiles to show
input int    InpRows       = 70;                   // Rows per profile
input ENUM_VP_ROWS InpRowMode = ROWS_AUTO;         // Row size (Auto = avg range of previous 5 profiles / rows; Fixed ticks; Row count = this profile's own range / rows, tick-aligned)
input int    InpRowTicks   = 10;                   // Fixed row size (ticks)
input double InpVaPct      = 70.0;                 // Value Area %
input ENUM_VP_VA InpVaMethod = VA_TV;              // Value Area method (TradingView documented / Target-reached 1 row / Pair 2 rows)
input ENUM_TIMEFRAMES InpLtf = PERIOD_CURRENT;     // Intrabar timeframe (current = Auto)
input ENUM_VP_VSRC InpVolSrc = SRC_FALLBACK;       // Volume source (Fallback to chart bar / Strict intrabars / Chart bars only)
input ENUM_VP_SESSBND InpSessBound = SESSB_EXCLUDE; // Session boundary candle without intrabars: Exclude (safe) or Estimate (prorated, flagged)
input double InpVolTol     = 2.0;                  // Intrabar volume tolerance (%) for the fallback check
input ENUM_VP_VTYPE InpVolType = VT_AUTO;          // Volume type (Auto: real only when EVERY bar of the profile has it)
input double InpTickOverride = 0.0;                // Price tick size override (0 = from symbol)
input int    InpMaxBars    = 5000;                 // Chart bars to analyse
input int    InpNakedLook  = 30;                   // Older profiles scanned for naked POC
input group "Histogram"
input bool   InpShowHist   = true;
input ENUM_VP_DIR InpDir   = DIR_LEFT_RIGHT;       // Direction
input int    InpWidthPct   = 35;                   // Width (% of period length)
input bool   InpSplitUD    = true;                 // Split Up / Down volume
input color  InpUpColor    = C'8,153,129';
input color  InpDnColor    = C'242,54,69';
input color  InpVaColor    = C'41,98,255';        // Inside VA (single colour)
input color  InpOutColor   = C'120,123,134';      // Outside VA
input color  InpPocRowColor = C'255,152,0';
input int    InpInTransp   = 35;                   // Inside-VA transparency (0-95)
input int    InpOutTransp  = 70;                   // Outside-VA transparency (0-95)
input group "Levels"
input bool   InpShowPoc    = true;
input color  InpPocColor   = C'255,152,0';
input int    InpPocWidth   = 2;
input bool   InpShowVA     = true;                 // VAH / VAL lines
input color  InpVaLineColor = C'41,98,255';
input int    InpVaLineWidth = 1;
input bool   InpShadeVA    = true;
input bool   InpExtendPrev = true;                 // Extend the latest finished profile into the current period
input int    InpLineExt    = 10;                   // Extend lines into the future (bars)
input bool   InpShowLabels = true;
input int    InpLabelSize  = 8;                    // Price label font size
input bool   InpShowDev    = false;                // Developing POC / VA lines
input group "Nodes (HVN / LVN)"
input bool   InpNodes      = true;                 // Mark HVN / LVN
input ENUM_VP_NODEMODE InpNodeMode = NODE_ADV;     // Advanced (prominence, separation, zones) or Simple (legacy local extrema)
input int    InpNodeProfs  = 2;                    // Profiles with nodes (newest)
input double InpNodeSmoothPct = 2.0;               // Advanced: smoothing half-width (% of rows, min 1 row)
input double InpNodeProm   = 15.0;                 // Advanced: min prominence (% of the highest smoothed row)
input double InpNodeSep    = 8.0;                  // Advanced: min separation between HVNs (% of rows)
input double InpNodeZonePct = 70.0;                // Advanced: zone = rows around the peak above this % of the peak
input bool   InpNodeZones  = true;                 // Advanced: shade node zones
input double InpNodeWinPct = 5.0;                  // Simple: window (% of rows, each side, min 2 rows)
input double InpHvnPct     = 40.0;                 // HVN: min % of the highest (smoothed) row
input double InpLvnPct     = 50.0;                 // LVN: max % of the highest (smoothed) row
input color  InpHvnColor   = C'0,188,212';
input color  InpLvnColor   = C'233,30,99';
input group "Naked POC"
input bool   InpNaked      = true;                 // Naked POC (not yet revisited)
input int    InpNakedMax   = 6;                    // Max active naked POCs shown
input ENUM_VP_TOUCH InpNakedTouch = NT_WICK;       // Revisit rule: any wick touch, or a close through the level
input int    InpNakedMaxAge = 0;                   // Hide naked POCs older than N bars (0 = never)
input int    InpNakedConsumed = 0;                 // Also show the last N consumed (touched) POCs, faded
input bool   InpNakedProv  = false;                // Flag a wick touch by the forming bar as "provisional" (never changes the confirmed state)
input color  InpNakedColor = C'255,152,0';
input group "Analytics (panel)"
input bool   InpMigration  = true;                 // POC migration vs previous profile
input double InpMigTol     = 1.0;                  // POC counts as stable within this many rows
input bool   InpVaShift    = true;                 // Value Area shift vs previous profile
input bool   InpShape      = false;                // Profile shape (heuristic: D / P / b / double distribution)
input bool   InpRvol       = false;                // Relative volume vs the same elapsed time in previous profiles
input int    InpRvolN      = 5;                    // Previous profiles averaged for relative volume
input group "Initial Balance"
input bool   InpIB         = false;                // Initial Balance of the profile period (best with Session mode)
input int    InpIBMin      = 60;                   // IB duration (minutes from the period start)
input bool   InpIBExt      = true;                 // Show 1x and 2x range extensions
input color  InpIBColor    = C'171,71,188';
input group "Confluence zones"
input bool   InpConf       = false;                // Mark places where several key levels overlap
input double InpConfTol    = 0.25;                 // Cluster tolerance (x ATR14)
input int    InpConfMin    = 3;                    // Min number of levels in a zone
input int    InpConfMax    = 4;                    // Max zones shown (nearest to price)
input color  InpConfColor  = C'255,214,0';
input group "Composite profile"
input bool   InpComp       = false;                // Composite of several profiles (no double counting)
input ENUM_VP_COMP InpCompMode = COMP_LASTN;       // Last N profiles / current week / current month
input int    InpCompN      = 5;                    // N (Last N profiles mode; the running profile counts as one)
input int    InpCompRows   = 70;                   // Composite rows
input bool   InpCompHist   = false;                // Composite histogram (right side)
input color  InpCompColor  = C'0,150,136';
input group "Anchored profile"
input bool   InpAnc        = false;                // Profile from a chosen date/time to now (uses intrabars)
input datetime InpAncTime  = 0;                    // Anchor date/time (broker server time)
input int    InpAncRows    = 70;                   // Anchored rows
input bool   InpAncHist    = false;                // Anchored histogram
input color  InpAncColor   = C'255,112,67';
input group "Multi-session profiles"
input bool   InpMsHist     = false;                // Histograms for session profiles (default: lines only)
input bool   InpMs1On      = false;                // Session 1 (Asia)
input string InpMs1Start   = "00:00";
input string InpMs1End     = "09:00";
input ENUM_VP_TZ InpMs1Tz  = TZ_TOKYO;
input color  InpMs1Color   = C'171,71,188';
input bool   InpMs2On      = false;                // Session 2 (London)
input string InpMs2Start   = "08:00";
input string InpMs2End     = "16:30";
input ENUM_VP_TZ InpMs2Tz  = TZ_LONDON;
input color  InpMs2Color   = C'41,98,255';
input bool   InpMs3On      = false;                // Session 3 (New York)
input string InpMs3Start   = "09:30";
input string InpMs3End     = "16:00";
input ENUM_VP_TZ InpMs3Tz  = TZ_NEWYORK;
input color  InpMs3Color   = C'38,166,91';
input bool   InpMs4On      = false;                // Session 4 (custom)
input string InpMs4Start   = "13:00";
input string InpMs4End     = "17:00";
input ENUM_VP_TZ InpMs4Tz  = TZ_UTC;
input color  InpMs4Color   = C'255,152,0';
input group "Alerts"
input ENUM_VP_ALERT InpAlertMode = AL_CLOSE;       // Level alerts: Confirmed close (candle closed beyond the level) / Live cross (price crossed now, may repeat)
input bool   InpAlertBlockPoor = true;              // Do NOT alert on a previous profile whose data is incomplete (missing/partial intrabars, chart-bar fallback, low coverage)
input bool   InpAlertNewPeriod = false;            // Alert when a new period starts (the previous profile levels change)
input bool   InpAlertHealth = true;                // Alert when the data/time basis becomes unreliable (Strict unavailable, server-time mismatch, incomplete previous profile)
input bool   InpAlertPopup = true;
input bool   InpAlertPush  = false;
input bool   InpAlertMail  = false;
input group "Status panel"
input bool   InpShowPanel  = false;                // Off by default: turn on from the input settings if you want the status table
input ENUM_PANEL_POS InpPanelPos = POS_TOP_LEFT;   // Top-left (default): the profile/histogram/labels are drawn near the current price and time (right side), so the panel stays clear of them there
input int    InpPanelFont  = 7;
input bool   InpShowTickRow = false;               // Tick size diagnostic row (debugging only)
input color  InpPanelBg    = C'21,26,32';
input group "Watermark"
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)

string PFX = "CRX_VP_";                 // made unique per indicator instance in OnInit
const color  BRAND_GREEN = C'11,163,28';

double BufPoc[], BufVah[], BufVal[];

//---- chart bars (chronological, index 0 = oldest)
datetime cTm[];
double   cHi[], cLo[], cOp[], cCl[], cVolR[], cVolT[];
int      g_bufOff = 0;                       // chart-bar index 0 maps to indicator buffer index g_bufOff
int      N = 0;
int      g_chartSec = 60;
ENUM_VP_PERIOD g_effPeriod = VP_DAY;    // resolved period: InpPeriod, or (if Auto) the period picked from the chart's own timeframe

//---- lower timeframe bars (chronological)
datetime lT[];
double   lH[], lL[], lVr[], lVt[];
bool     lU[];
int      lN = 0;
ENUM_TIMEFRAMES g_ltf = PERIOD_CURRENT;
bool     g_useLtf = false;
bool     g_strictNA = false;                     // CRX-009: Strict requested but no lower timeframe is usable

//---- profiles (scalar struct; rows are stored in flat arrays)
struct SProf
  {
   int      i0, i1;            // first / last chart bar index
   datetime t0, t1;
   double   rs, off;
   int      base;              // row index of the first stored row
   int      rowStart, rowN;
   double   total, upTotal, hi, lo;
   double   poc, vah, val;
   int      pocI, vaLo, vaHi;
   double   vaCov;             // share of the profile volume inside [VAL, VAH] (%)
   int      vtype;             // 1 = real volume, 0 = tick volume
   bool     valid;             // row volumes add up to the total
   bool     naked;             // POC not revisited (confirmed on closed bars only)
   bool     prov;              // the forming bar is touching the POC right now (provisional, not part of the confirmed state)
   int      touchBar;          // first chart bar that revisited the POC (-1 = none)
   int      kind;              // 0 primary, 1 composite, 2 anchored, 3 session slot
   int      slot;              // session slot (0..3)
   int      cumStart, cumN;    // cumulative chart-bar volume per bar since the profile start (relative volume)
   double   ibH, ibL;          // Initial Balance (primary profiles)
   long     qIntra, qFb, qSkip, qExp, qGot, qSparse, qPartial, qTypeMis;   // CRX-017: data quality of THIS profile
   double   qVolIn, qVolChart;
   bool     ibEst;             // CRX-012: IB lost a partially-covered candle (no intrabars) -> approximate
   int      bPrecise, bEstimated, bExcluded;   // session-boundary candle quality: precise (intrabar-filtered), estimated (prorated), excluded
   int      clamped;           // CRX-003: data that fell outside this profile's own grid (should be ~0; >0 makes the profile invalid)
  };
SProf  g_p[];
int    g_np = 0;               // PRIMARY profiles (indexes 0..g_np-1); extra profiles follow
double g_up[], g_dn[];         // flat row volumes of all profiles
double g_cum[];                // flat cumulative volumes
int    g_compIdx = -1, g_ancIdx = -1;
int    g_msA[4], g_msB[4];     // session slots: latest session and the one before (-1 = none)
bool   g_msRun[4];             // the latest session of the slot is still running
int    g_tot = 0;              // all profiles
bool   g_sessOK = false;
int    g_sessS = 0, g_sessE = 0;
datetime g_lastBar = 0;
datetime g_lastAlertBar = 0;
uint   g_lastPaint = 0;
long   g_ltfBars = 0, g_fbBars = 0, g_skipBars = 0, g_clamped = 0;
long   g_cvExp = 0, g_cvGot = 0, g_cvSparse = 0, g_cvPartial = 0, g_cvTypeMis = 0;   // CRX-008: intrabar coverage accounting
double g_cvVolIn = 0, g_cvVolChart = 0;
int    g_curIdx = -1, g_doneIdx = -1;     // current (running) profile and latest FINISHED profile; -1 = none
string g_status = "";
bool   g_retry = false;
uint   g_lastTry = 0;
bool   g_alertInit = false;
double g_livePx = 0;
datetime g_liveBar[6];
int    g_healthSeen = 0;
bool   g_weekSun = false;
double g_tick = 0.0;
double g_tickRaw = 0.0;
string g_tickSrc = "";
int    g_nReal = 0, g_nTick = 0;
int    g_ancState = 0, g_ancLostSec = 0;                  // CRX-016: 1 exact, 2 gap->next candle, 3 snapped fwd (lost sec), 4 before history, 5 after last candle, 6 cut by intrabars
int    g_compN = 0;
int    g_compMixEx = 0, g_compCut = 0, g_realEmpty = 0;                 // CRX-010: composite sources excluded for a different volume type / Real-only profiles without volume
string g_volKind = "Tick volume (approx.)";

//+------------------------------------------------------------------+
bool IsCryptoSymbol()
  {
   string s = _Symbol;
   string keys[] = {"BTC", "ETH", "LTC", "XRP", "SOL", "DOGE", "ADA", "BNB", "TRX", "DOT"};
   for(int i = 0; i < ArraySize(keys); i++)
      if(StringFind(s, keys[i]) >= 0) return true;
   return false;
  }

// CRX-027: validated copies of numeric inputs (input variables are read-only); filled by ValidateInputs() at start of OnInit
int vRows;
int vRowTicks;
double vVaPct;
double vVolTol;
int vMaxBars;
int vRollN;
int vKeepPrev;
int vNakedLook;
int vNakedMax;
int vCompN;
int vCompRows;
int vAncRows;
int vRvolN;
int vIBMin;
double vConfTol;
int vConfMin;
int vConfMax;
double vNodeSmoothPct;
double vNodeProm;
double vNodeSep;
double vNodeZonePct;
double vNodeWinPct;
double vHvnPct;
double vLvnPct;
int vWidthPct;
int vInTransp;
int vOutTransp;
int vLineExt;
int vPocWidth;
int vVaLineWidth;
int vLabelSize;
int vPanelFont;
double vMigTol;
int vNodeProfs;
double vFixedOffset;
double vServerOffset;
int vNakedConsumed;
string g_inNote = "";
void AddNote(const string t)
  {
   if(g_inNote != "") g_inNote += ", ";
   g_inNote += t;
  }
void ClampI(int &dst, const int v, const int lo, const int hi, const string name)
  {
   int r = v < lo ? lo : (v > hi ? hi : v);
   if(r != v) AddNote(name + " " + IntegerToString(v) + "->" + IntegerToString(r));
   dst = r;
  }
void ClampD(double &dst, const double v, const double lo, const double hi, const string name)
  {
   double r = !MathIsValidNumber(v) ? lo : (v < lo ? lo : (v > hi ? hi : v));
   if(r != v) AddNote(name + " " + DoubleToString(v, 2) + "->" + DoubleToString(r, 2));
   dst = r;
  }
void ValidateInputs()
  {
   g_inNote = "";
   ClampI(vRows, InpRows, 2, 2000, "Rows");
   ClampI(vRowTicks, InpRowTicks, 1, 100000, "RowTicks");
   ClampD(vVaPct, InpVaPct, 1, 100, "VaPct");
   ClampD(vVolTol, InpVolTol, 0, 100, "VolTol");
   ClampI(vMaxBars, InpMaxBars, 100, 100000, "MaxBars");
   ClampI(vRollN, InpRollN, 2, 20000, "RollN");
   ClampI(vKeepPrev, InpKeepPrev, 0, 50, "KeepPrev");
   ClampI(vNakedLook, InpNakedLook, 0, 500, "NakedLook");
   ClampI(vNakedMax, InpNakedMax, 0, 50, "NakedMax");
   ClampI(vCompN, InpCompN, 1, 200, "CompN");
   ClampI(vCompRows, InpCompRows, 2, 2000, "CompRows");
   ClampI(vAncRows, InpAncRows, 2, 2000, "AncRows");
   ClampI(vRvolN, InpRvolN, 1, 50, "RvolN");
   ClampI(vIBMin, InpIBMin, 1, 1440, "IBMin");
   ClampD(vConfTol, InpConfTol, 0.01, 5, "ConfTol");
   ClampI(vConfMin, InpConfMin, 2, 10, "ConfMin");
   ClampI(vConfMax, InpConfMax, 1, 10, "ConfMax");
   ClampD(vNodeSmoothPct, InpNodeSmoothPct, 0, 20, "NodeSmoothPct");
   ClampD(vNodeProm, InpNodeProm, 0, 100, "NodeProm");
   ClampD(vNodeSep, InpNodeSep, 0, 50, "NodeSep");
   ClampD(vNodeZonePct, InpNodeZonePct, 1, 100, "NodeZonePct");
   ClampD(vNodeWinPct, InpNodeWinPct, 0, 50, "NodeWinPct");
   ClampD(vHvnPct, InpHvnPct, 0, 100, "HvnPct");
   ClampD(vLvnPct, InpLvnPct, 0, 100, "LvnPct");
   ClampI(vWidthPct, InpWidthPct, 1, 100, "WidthPct");
   ClampI(vInTransp, InpInTransp, 0, 95, "InTransp");
   ClampI(vOutTransp, InpOutTransp, 0, 95, "OutTransp");
   ClampI(vLineExt, InpLineExt, 0, 500, "LineExt");
   ClampI(vPocWidth, InpPocWidth, 1, 5, "PocWidth");
   ClampI(vVaLineWidth, InpVaLineWidth, 1, 5, "VaLineWidth");
   ClampI(vLabelSize, InpLabelSize, 5, 24, "LabelSize");
   ClampI(vPanelFont, InpPanelFont, 6, 24, "PanelFont");
   ClampD(vMigTol, InpMigTol, 0, 10, "MigTol");
   ClampI(vNodeProfs, InpNodeProfs, 0, 10, "NodeProfs");
   ClampD(vFixedOffset, InpFixedOffset, -14, 14, "FixedOffset");
   ClampD(vServerOffset, InpServerOffset, -14, 14, "ServerOffset");
   ClampI(vNakedConsumed, InpNakedConsumed, 0, 50, "NakedConsumed");
  }

int ParseHM(const string s)
  {
   string p[];
   if(StringSplit(s, ':', p) < 2) { AddNote("time '" + s + "' invalid->00:00"); return 0; }
   int h = (int)StringToInteger(p[0]), m = (int)StringToInteger(p[1]);
   if(h < 0 || h > 23 || m < 0 || m > 59) { AddNote("time '" + s + "' out of range->00:00"); return 0; }
   return h * 60 + m;
  }

ENUM_TIMEFRAMES AutoLtf(const int sec)
  {
   if(sec <= 60) return PERIOD_CURRENT;
   if(sec / 60 <= 100)   return PERIOD_M1;
   if(sec / 180 <= 100)  return PERIOD_M3;
   if(sec / 300 <= 100)  return PERIOD_M5;
   if(sec / 900 <= 100)  return PERIOD_M15;
   if(sec / 1800 <= 100) return PERIOD_M30;
   if(sec / 3600 <= 100) return PERIOD_H1;
   return PERIOD_H4;
  }

string Px(const double v)
  {
   if(v == EMPTY_VALUE || v != v) return "—";
   return DoubleToString(v, _Digits);
  }

color Blend(const color c, const color bg, const int transp)
  {
   double a = 1.0 - transp / 100.0;
   int ci = (int)c, bi = (int)bg;
   int r1 = ci & 0xFF, g1 = (ci >> 8) & 0xFF, b1 = (ci >> 16) & 0xFF;
   int r2 = bi & 0xFF, g2 = (bi >> 8) & 0xFF, b2 = (bi >> 16) & 0xFF;
   int r = (int)MathRound(r1 * a + r2 * (1.0 - a)), g = (int)MathRound(g1 * a + g2 * (1.0 - a)), b = (int)MathRound(b1 * a + b2 * (1.0 - a));
   return (color)(r | (g << 8) | (b << 16));
  }

//+------------------------------------------------------------------+
//| Time basis (UTC / London / New York / Tokyo with DST)            |
//+------------------------------------------------------------------+
long DaysFromCivil(int y, const int m, const int d)
  {
   if(m <= 2) y--;
   long era = (y >= 0 ? y : y - 399) / 400;
   long yoe = y - era * 400;
   long doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
   long doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
   return era * 146097 + doe - 719468;
  }

int WeekdayOfDay(const long days)                        // 0 = Sunday
  {
   long w = (days + 4) % 7;
   if(w < 0) w += 7;
   return (int)w;
  }

long NthSundayDay(const int y, const int m, const int n) // n-th Sunday of the month (1-based), as a day number
  {
   long d1 = DaysFromCivil(y, m, 1);
   long first = d1 + ((7 - WeekdayOfDay(d1)) % 7);
   return first + (long)(n - 1) * 7;
  }

long LastSundayDay(const int y, const int m)
  {
   int ny = (m == 12) ? y + 1 : y, nm = (m == 12) ? 1 : m + 1;
   long last = DaysFromCivil(ny, nm, 1) - 1;
   return last - WeekdayOfDay(last);
  }

bool IsDstEU(const datetime utc)                         // last Sunday of March 01:00 UTC -> last Sunday of October 01:00 UTC
  {
   MqlDateTime d;
   TimeToStruct(utc, d);
   datetime a = (datetime)(LastSundayDay(d.year, 3) * 86400 + 3600);
   datetime b = (datetime)(LastSundayDay(d.year, 10) * 86400 + 3600);
   return utc >= a && utc < b;
  }

bool IsDstUS(const datetime utc)                         // 2nd Sunday of March 07:00 UTC -> 1st Sunday of November 06:00 UTC
  {
   MqlDateTime d;
   TimeToStruct(utc, d);
   datetime a = (datetime)(NthSundayDay(d.year, 3, 2) * 86400 + 7 * 3600);
   datetime b = (datetime)(NthSundayDay(d.year, 11, 1) * 86400 + 6 * 3600);
   return utc >= a && utc < b;
  }

// CRX-013: manual server-offset schedule. Entries: "yyyy.mm.dd hh:mi=offsetHours" separated by ';', times in UTC, sorted on load.
// Before the first entry InpServerOffset applies. The repeated hour at a fall-back is resolved to its FIRST occurrence.
datetime g_schT[];
double   g_schO[];
int      g_schN = 0;

void ParseSchedule()
  {
   g_schN = 0;
   ArrayResize(g_schT, 0); ArrayResize(g_schO, 0);
   if(InpServerDst != DST_MANUAL) return;
   string parts[];
   int np = StringSplit(InpOffsetSched, ';', parts);
   for(int i = 0; i < np; i++)
     {
      string kv[];
      if(StringSplit(parts[i], '=', kv) != 2) continue;
      datetime t = StringToTime(kv[0]);
      if(t <= 0) continue;
      int n = g_schN;
      ArrayResize(g_schT, n + 1); ArrayResize(g_schO, n + 1);
      int pos = n;
      while(pos > 0 && g_schT[pos - 1] > t) { g_schT[pos] = g_schT[pos - 1]; g_schO[pos] = g_schO[pos - 1]; pos--; }
      g_schT[pos] = t; g_schO[pos] = StringToDouble(kv[1]);
      g_schN++;
     }
  }

double ManualOffsetAtServer(const datetime t)           // server offset (hours) in force at SERVER time t
  {
   double off = vServerOffset;
   for(int i = 0; i < g_schN; i++)
     {
      if(t >= g_schT[i] + (datetime)MathRound(off * 3600.0)) off = g_schO[i]; else break;
     }
   return off;
  }

datetime ServerToUtc(const datetime t)
  {
   if(InpServerDst == DST_MANUAL) return t - (datetime)MathRound(ManualOffsetAtServer(t) * 3600.0);
   long base = (long)MathRound(vServerOffset * 3600.0);
   if(InpServerDst != DST_NONE)
     {
      datetime u = t - (datetime)(base + 3600);                    // reading "the server is on summer time"
      if((InpServerDst == DST_US && IsDstUS(u)) || (InpServerDst == DST_EU && IsDstEU(u))) return u;
     }
   return t - (datetime)base;                                      // otherwise winter time (the repeated hour at the DST end is ambiguous by nature)
  }

datetime ToLocalTz(const datetime t, const ENUM_VP_TZ tz)
  {
   if(tz == TZ_SERVER) return t;
   datetime utc = ServerToUtc(t);
   if(tz == TZ_UTC) return utc;
   if(tz == TZ_LONDON)  return utc + (IsDstEU(utc) ? 3600 : 0);
   if(tz == TZ_NEWYORK) return utc + (IsDstUS(utc) ? -4 * 3600 : -5 * 3600);
   if(tz == TZ_TOKYO)   return utc + 9 * 3600;
   return utc + (datetime)MathRound(vFixedOffset * 3600.0);
  }

// CRX-013: the offset model (InpServerOffset + DST rule / manual schedule) is an ASSUMPTION about the broker. When the terminal's own
// GMT clock is available, compare the two: 1 = model agrees with the terminal now, 2 = MISMATCH, 0 = cannot be judged (stale or odd clock).
int    g_tzState = 0;
double g_tzObs = 0, g_tzMdl = 0;
void CheckServerTz()
  {
   g_tzState = 0;
   datetime ts = TimeTradeServer(), gmt = TimeGMT();
   if(ts <= 0 || gmt <= 0) return;
   long obs = (long)(ts - gmt), mdl = (long)(ts - ServerToUtc(ts));
   long q = (long)MathRound(obs / 900.0) * 900;
   if(MathAbs((double)(obs - q)) > 120.0 || MathAbs((double)obs) > 14 * 3600) return;           // not a clean quarter-hour offset: stale tick / clock skew
   g_tzObs = obs / 3600.0; g_tzMdl = mdl / 3600.0;
   g_tzState = (MathAbs((double)(q - mdl)) < 900.0) ? 1 : 2;
  }

bool UsesTzConversion()
  {
   if(InpTz != TZ_SERVER) return true;
   if(InpMs1On && InpMs1Tz != TZ_SERVER) return true;
   if(InpMs2On && InpMs2Tz != TZ_SERVER) return true;
   if(InpMs3On && InpMs3Tz != TZ_SERVER) return true;
   if(InpMs4On && InpMs4Tz != TZ_SERVER) return true;
   return false;
  }

datetime ToLocal(const datetime t)
  {
   return ToLocalTz(t, InpTz);
  }

// period id of a candle open time for a period kind (Day / Week / Month / Session); -1 = outside the session
long PeriodIdKind(const ENUM_VP_PERIOD kind, const datetime t)
  {
   datetime tl = ToLocal(t);
   MqlDateTime d;
   TimeToStruct(tl, d);
   long days = (long)(tl / 86400);
   if(kind == VP_DAY)   return days;
   if(kind == VP_WEEK)  return g_weekSun ? (days + 4) / 7 : (days + 3) / 7;                  // 1970-01-01 was a Thursday
   if(kind == VP_MONTH) return (long)d.year * 12 + d.mon;
   int m = d.hour * 60 + d.min;                                                              // session
   if(g_sessS < g_sessE) return (m >= g_sessS && m < g_sessE) ? days : -1;
   if(m >= g_sessS) return days;
   if(m < g_sessE)  return days - 1;
   return -1;
  }

long PeriodId(const datetime t)
  {
   return PeriodIdKind(g_effPeriod, t);
  }

//+------------------------------------------------------------------+
//| Session slots (multi-session profiles)                           |
//+------------------------------------------------------------------+
int g_slotS[4], g_slotE[4];

bool SlotOn(const int s)
  {
   if(s == 0) return InpMs1On;
   if(s == 1) return InpMs2On;
   if(s == 2) return InpMs3On;
   return InpMs4On;
  }
string SlotName(const int s)
  {
   if(s == 0) return "Asia";
   if(s == 1) return "London";
   if(s == 2) return "New York";
   return "Custom";
  }
ENUM_VP_TZ SlotTz(const int s)
  {
   if(s == 0) return InpMs1Tz;
   if(s == 1) return InpMs2Tz;
   if(s == 2) return InpMs3Tz;
   return InpMs4Tz;
  }
color SlotColor(const int s)
  {
   if(s == 0) return InpMs1Color;
   if(s == 1) return InpMs2Color;
   if(s == 2) return InpMs3Color;
   return InpMs4Color;
  }
string SlotStartText(const int s)
  {
   if(s == 0) return InpMs1Start;
   if(s == 1) return InpMs2Start;
   if(s == 2) return InpMs3Start;
   return InpMs4Start;
  }
string SlotEndText(const int s)
  {
   if(s == 0) return InpMs1End;
   if(s == 1) return InpMs2End;
   if(s == 2) return InpMs3End;
   return InpMs4End;
  }

// session id of a candle open time for slot s (-1 = outside the session)
long SlotId(const datetime t, const int s)
  {
   datetime tl = ToLocalTz(t, SlotTz(s));
   MqlDateTime d;
   TimeToStruct(tl, d);
   long days = (long)(tl / 86400);
   int m = d.hour * 60 + d.min, S = g_slotS[s], E = g_slotE[s];
   if(S < E) return (m >= S && m < E) ? days : -1;
   if(m >= S) return days;
   if(m < E)  return days - 1;
   return -1;
  }

// filt: 0 = primary period definition, 1..4 = session slot
long IdOf(const datetime t, const int filt)
  {
   if(filt == 200) return t >= InpAncTime ? -2 : -1;          // CRX-016: anchored profile - everything from the anchor instant on
   return filt == 0 ? PeriodId(t) : SlotId(t, filt - 1);
  }

// CRX-017: data-quality text of ONE profile (so an old, poorly covered profile is not hidden by a good global average)
string QualityText(const int idx)
  {
   if(idx < 0) return "";
   long n = g_p[idx].qIntra + g_p[idx].qFb + g_p[idx].qSkip;
   if(n <= 0) return g_useLtf && g_effPeriod != VP_ROLL ? "no candle data" : "chart bars";
   string t = IntegerToString((int)(g_p[idx].qIntra * 100 / n)) + "% intrabar";
   if(g_p[idx].qExp > 0) t += " · time " + DoubleToString(100.0 * g_p[idx].qGot / g_p[idx].qExp, 1) + "%";
   if(g_p[idx].qVolChart > 0) t += " · vol " + DoubleToString(100.0 * g_p[idx].qVolIn / g_p[idx].qVolChart, 1) + "%";
   if(g_p[idx].qFb > 0) t += " · " + IntegerToString((int)g_p[idx].qFb) + " chart-bar";
   if(g_p[idx].qSkip > 0) t += " · " + IntegerToString((int)g_p[idx].qSkip) + " MISSING";
   if(g_p[idx].qPartial > 0) t += " · " + IntegerToString((int)g_p[idx].qPartial) + " PARTIAL";
   if(g_p[idx].qTypeMis > 0) t += " · " + IntegerToString((int)g_p[idx].qTypeMis) + " vol-type mismatch";
   return t;
  }

bool QualityPoor(const int idx)
  {
   if(idx < 0) return false;
   return g_p[idx].qSkip > 0 || g_p[idx].qPartial > 0 || g_p[idx].qTypeMis > 0 || (g_p[idx].qFb > 0) ||
          (g_p[idx].qExp > 0 && g_p[idx].qGot < g_p[idx].qExp * 0.9) || (g_p[idx].qVolChart > 0 && g_p[idx].qVolIn < g_p[idx].qVolChart * 0.95);
  }

//+------------------------------------------------------------------+
int OnInit()
  {
   PFX = "CRX_VP" + IntegerToString((int)(GetMicrosecondCount() % 1000000)) + "_";          // unique per instance
   SetIndexBuffer(0, BufPoc, INDICATOR_DATA);
   SetIndexBuffer(1, BufVah, INDICATOR_DATA);
   SetIndexBuffer(2, BufVal, INDICATOR_DATA);
   ArraySetAsSeries(BufPoc, false);
   ArraySetAsSeries(BufVah, false);
   ArraySetAsSeries(BufVal, false);
   for(int p = 0; p < 3; p++) PlotIndexSetDouble(p, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   if(!InpShowDev)
      for(int p = 0; p < 3; p++) PlotIndexSetInteger(p, PLOT_DRAW_TYPE, DRAW_NONE);
   PlotIndexSetInteger(0, PLOT_LINE_COLOR, 0, InpPocColor);
   PlotIndexSetInteger(1, PLOT_LINE_COLOR, 0, InpVaLineColor);
   PlotIndexSetInteger(2, PLOT_LINE_COLOR, 0, InpVaLineColor);

   ValidateInputs();
   ParseSchedule();
   g_chartSec = PeriodSeconds(_Period);
   // Auto: picks the period that always has finer candles inside it than the chart itself, so the
   // "Chart TF ≥ period" guard never blanks the indicator, no matter what timeframe it is attached to.
   g_effPeriod = (InpPeriod != VP_AUTO) ? InpPeriod : (g_chartSec < 86400 ? VP_DAY : g_chartSec < 604800 ? VP_WEEK : g_chartSec < 2592000 ? VP_MONTH : VP_ROLL);
   g_sessS = ParseHM(InpSessStart);
   g_sessE = ParseHM(InpSessEnd);
   for(int q = 0; q < 4; q++) { g_slotS[q] = ParseHM(SlotStartText(q)); g_slotE[q] = ParseHM(SlotEndText(q)); }
   g_ltf = (InpLtf == PERIOD_CURRENT) ? AutoLtf(g_chartSec) : InpLtf;
   g_useLtf = (g_effPeriod != VP_ROLL) && InpVolSrc != SRC_CHART && g_ltf != PERIOD_CURRENT && PeriodSeconds(g_ltf) < g_chartSec;
   g_strictNA = (InpVolSrc == SRC_STRICT && g_effPeriod != VP_ROLL && !g_useLtf && g_chartSec > 60);   // on M1 the chart bars ARE the finest data, so Strict is trivially met
   //---- price tick size: override, else the symbol tick size, else Point
   g_tickRaw = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);                      // MT5: this constant is documented in price units, no conversion needed
   g_tick = (g_tickRaw > 0 && MathIsValidNumber(g_tickRaw)) ? g_tickRaw : _Point;
   if(InpTickOverride > 0 && MathIsValidNumber(InpTickOverride) && InpTickOverride < 1e9) { g_tick = InpTickOverride; g_tickSrc = "Override"; }
   else g_tickSrc = "SYMBOL_TRADE_TICK_SIZE";
   if(g_tick <= 0 || !MathIsValidNumber(g_tick)) { g_tick = _Point; g_tickSrc = "Point (fallback)"; }
   //---- week start
   if(InpWeekStart == WK_SUNDAY) g_weekSun = true;
   else if(InpWeekStart == WK_MONDAY) g_weekSun = false;
   else
     {
      datetime f, t;
      bool sat = SymbolInfoSessionTrade(_Symbol, SATURDAY, 0, f, t);                         // a Saturday session = 24/7 market
      g_weekSun = !(sat || IsCryptoSymbol());
     }
   IndicatorSetString(INDICATOR_SHORTNAME, "Crypex Volume Profile");
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
   return INIT_SUCCEEDED;
  }

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

//+------------------------------------------------------------------+
//| Data loading                                                     |
//+------------------------------------------------------------------+
bool LoadChart()
  {
   MqlRates r[];
   ArraySetAsSeries(r, false);
   int total = Bars(_Symbol, _Period);
   int want = MathMin(vMaxBars, total);
   if(want < 5) { g_status = "History not ready"; g_retry = true; return false; }
   int got = CopyRates(_Symbol, _Period, 0, want, r);
   if(got < 5) { g_status = "History not ready"; g_retry = true; return false; }
   if(got < want) { g_status = "History loading " + IntegerToString(got) + "/" + IntegerToString(want) + " bars"; g_retry = true; }
   N = got;
   ArrayResize(cTm, N); ArrayResize(cHi, N); ArrayResize(cLo, N); ArrayResize(cOp, N); ArrayResize(cCl, N); ArrayResize(cVolR, N); ArrayResize(cVolT, N);
   for(int i = 0; i < N; i++)
     {
      cTm[i] = r[i].time; cHi[i] = r[i].high; cLo[i] = r[i].low; cOp[i] = r[i].open; cCl[i] = r[i].close;
      cVolR[i] = (double)r[i].real_volume;
      cVolT[i] = (double)r[i].tick_volume;
     }
   return true;
  }

// lower-timeframe candles in [from, to)
void LoadLtf(const datetime from, const datetime to)
  {
   lN = 0;
   if(!g_useLtf) return;
   if(!(bool)SeriesInfoInteger(_Symbol, g_ltf, SERIES_SYNCHRONIZED)) { g_status = "Intrabar history is syncing"; g_retry = true; }
   MqlRates r[];
   ArraySetAsSeries(r, false);
   int got = CopyRates(_Symbol, g_ltf, from, to - 1, r);
   if(got <= 0)
     {
      if(g_status == "") g_status = (InpVolSrc == SRC_STRICT) ? "Strict: no intrabar data - those candles are MISSING (chart bars not used)" : "No intrabar data: using chart bars";
      g_retry = true;
      return;
     }
   ArrayResize(lT, got); ArrayResize(lH, got); ArrayResize(lL, got); ArrayResize(lVr, got); ArrayResize(lVt, got); ArrayResize(lU, got);
   for(int k = 0; k < got; k++)
     {
      lT[k] = r[k].time; lH[k] = r[k].high; lL[k] = r[k].low;
      lVr[k] = (double)r[k].real_volume; lVt[k] = (double)r[k].tick_volume;
      lU[k] = r[k].close >= r[k].open;
     }
   lN = got;
  }

// 1 = real volume, 0 = tick volume for the profile made of chart bars a..b
int ChooseVolType(const int a, const int b)
  {
   if(InpVolType == VT_TICK) return 0;
   if(InpVolType == VT_REAL) return 1;
   for(int i = a; i <= b; i++)
      if(cVolR[i] <= 0) return 0;                                    // Auto: real only when EVERY bar of the profile has it
   return 1;
  }

double ChartVol(const int i, const int vt)
  {
   return vt == 1 ? cVolR[i] : cVolT[i];
  }

//+------------------------------------------------------------------+
//| Profile maths (same algorithm as the TradingView version)         |
//+------------------------------------------------------------------+
// Spreads one candle's volume over the price rows by overlap with its High-Low range.
// Every candle is placed inside the grid; if a candle lies partly or fully outside it, its volume is moved to the nearest edge row
// (counted in g_clamped) so the total volume is always conserved.
void AddVol(const int k, const double h, const double l, const double v, const bool isUp)
  {
   int rowN = g_p[k].rowN;
   if(v <= 0 || h < l || rowN <= 0) return;
   double rs = g_p[k].rs, off = g_p[k].off;
   int base = g_p[k].base, s0 = g_p[k].rowStart;
   double posL = (l - off) / rs, posH = (h - off) / rs;
   int i0 = (int)MathFloor(posL + 1e-9) - base;
   int i1 = (int)MathFloor(posH + 1e-9) - base;
   if(i1 > i0 && (posH - (i1 + base)) < 1e-9) i1--;               // High exactly on a row boundary: the row above gets nothing
   bool whole = false;
   if(i1 < 0)               { i0 = 0; i1 = 0; g_clamped++; g_p[k].clamped++; whole = true; }
   else if(i0 > rowN - 1)   { i0 = rowN - 1; i1 = rowN - 1; g_clamped++; g_p[k].clamped++; whole = true; }
   else
     {
      if(i0 < 0)        { i0 = 0; g_clamped++; g_p[k].clamped++; }
      if(i1 > rowN - 1) { i1 = rowN - 1; g_clamped++; g_p[k].clamped++; }
     }
   if(i0 > i1) i0 = i1;
   double rng = h - l;
   if(whole || rng <= 0 || i0 == i1)
     {
      if(isUp) g_up[s0 + i0] += v; else g_dn[s0 + i0] += v;
     }
   else
     {
      double sum = 0;
      for(int i = i0; i <= i1; i++)
        {
         double rl = off + (base + i) * rs;
         double ov = MathMin(h, rl + rs) - MathMax(l, rl);
         if(ov > 0) sum += ov;
        }
      if(sum <= 0)
        {
         if(isUp) g_up[s0 + i0] += v; else g_dn[s0 + i0] += v;
        }
      else
         for(int i = i0; i <= i1; i++)
           {
            double rl = off + (base + i) * rs;
            double ov = MathMin(h, rl + rs) - MathMax(l, rl);
            if(ov <= 0) continue;
            double share = v * ov / sum;                          // shares add up to v exactly
            if(isUp) g_up[s0 + i] += share; else g_dn[s0 + i] += share;
           }
     }
   g_p[k].total += v;
   if(isUp) g_p[k].upTotal += v;
  }

// POC + Value Area for profile k. Returns the POC row index (-1 when empty) and fills lo / hi (row indexes).
// POC: highest volume; equal volumes (within 1e-12 of the profile total) -> the row closest to the profile centre, then the lower one.
// InpVaMethod: VA_TV (documented TradingView rule incl. its stop rule), VA_TARGET1 (1 row, until the target is reached), VA_PAIR2 (2 rows).
int CalcVA(const int k, int &lo, int &hi)
  {
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   int pocI = -1;
   lo = 0; hi = 0;
   if(n <= 0 || g_p[k].total <= 0) return -1;
   double eps = g_p[k].total * 1e-12;
   double best = -1.0, mid = (n - 1) / 2.0;
   for(int i = 0; i < n; i++)
     {
      double t = g_up[s0 + i] + g_dn[s0 + i];
      if(t > best + eps) { best = t; pocI = i; }
      else if(MathAbs(t - best) <= eps && MathAbs(i - mid) < MathAbs(pocI - mid)) { best = MathMax(best, t); pocI = i; }
     }
   double target = g_p[k].total * vVaPct / 100.0;
   lo = pocI; hi = pocI;
   if(InpVaMethod == VA_TV)
     {
      double remaining = target - best;
      while(remaining > eps && (lo > 0 || hi < n - 1))
        {
         bool hasA = hi < n - 1, hasB = lo > 0;
         double aV = hasA ? g_up[s0 + hi + 1] + g_dn[s0 + hi + 1] : -1.0;
         double bV = hasB ? g_up[s0 + lo - 1] + g_dn[s0 + lo - 1] : -1.0;
         bool takeAbove;
         if(!hasB) takeAbove = true;
         else if(!hasA) takeAbove = false;
         else if(aV > bV + eps) takeAbove = true;
         else if(bV > aV + eps) takeAbove = false;
         else takeAbove = (hi + 1 - pocI) <= (pocI - (lo - 1));       // tie: the row closer to the POC, then the upper one
         double v = takeAbove ? aV : bV;
         if(v > remaining + eps) break;                               // documented stop rule: the row would exceed the remaining target
         remaining -= v;
         if(takeAbove) hi++; else lo--;
        }
      return pocI;
     }
   double acc = best;
   int steps = (InpVaMethod == VA_PAIR2) ? 2 : 1;
   while(acc + eps < target && (lo > 0 || hi < n - 1))
     {
      double aV = 0, bV = 0;
      int aN = 0, bN = 0;
      for(int s = 1; s <= steps; s++)
        {
         if(hi + s <= n - 1) { aV += g_up[s0 + hi + s] + g_dn[s0 + hi + s]; aN++; }
         if(lo - s >= 0)     { bV += g_up[s0 + lo - s] + g_dn[s0 + lo - s]; bN++; }
        }
      bool takeAbove;
      if(aN == 0) takeAbove = false;
      else if(bN == 0) takeAbove = true;
      else if(aV > bV + eps) takeAbove = true;
      else if(bV > aV + eps) takeAbove = false;
      else takeAbove = (hi + 1 - pocI) <= (pocI - (lo - 1));
      if(takeAbove)
        {
         for(int s = 1; s <= aN; s++) acc += g_up[s0 + hi + s] + g_dn[s0 + hi + s];
         hi += aN;
        }
      else
        {
         for(int s = 1; s <= bN; s++) acc += g_up[s0 + lo - s] + g_dn[s0 + lo - s];
         lo -= bN;
        }
     }
   return pocI;
  }

void SetLevels(const int k)
  {
   int lo, hi;
   int pi = CalcVA(k, lo, hi);
   g_p[k].pocI = pi; g_p[k].vaLo = lo; g_p[k].vaHi = hi;
   g_p[k].vaCov = 0;
   if(pi < 0) { g_p[k].poc = EMPTY_VALUE; g_p[k].vah = EMPTY_VALUE; g_p[k].val = EMPTY_VALUE; return; }
   g_p[k].poc = g_p[k].off + (g_p[k].base + pi + 0.5) * g_p[k].rs;
   g_p[k].vah = g_p[k].off + (g_p[k].base + hi + 1) * g_p[k].rs;
   g_p[k].val = g_p[k].off + (g_p[k].base + lo) * g_p[k].rs;
   double vs = 0;
   for(int i = lo; i <= hi; i++) vs += g_up[g_p[k].rowStart + i] + g_dn[g_p[k].rowStart + i];
   g_p[k].vaCov = g_p[k].total > 0 ? vs / g_p[k].total * 100.0 : 0.0;
  }

// HVN = local maxima of the 1-2-1 smoothed profile; LVN = local minima between two HVNs
void FindNodes(const int k, int &hv[], int &lv[])
  {
   ArrayResize(hv, 0); ArrayResize(lv, 0);
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   if(n < 5) return;
   int nodeWin = MathMax(2, (int)MathRound(n * vNodeWinPct / 100.0));
   double sm[];
   ArrayResize(sm, n);
   double smMax = 0;
   for(int i = 0; i < n; i++)
     {
      double b = g_up[s0 + i] + g_dn[s0 + i];
      double a = i > 0 ? g_up[s0 + i - 1] + g_dn[s0 + i - 1] : b;
      double c = i < n - 1 ? g_up[s0 + i + 1] + g_dn[s0 + i + 1] : b;
      sm[i] = (a + 2 * b + c) / 4.0;
      if(sm[i] > smMax) smMax = sm[i];
     }
   if(smMax <= 0) return;
   int raw[];
   ArrayResize(raw, 0);
   for(int i = 0; i < n; i++)
     {
      double s = sm[i];
      bool isMax = true, isMin = true;
      int j0 = MathMax(0, i - nodeWin), j1 = MathMin(n - 1, i + nodeWin);
      for(int j = j0; j <= j1; j++)
        {
         if(j == i) continue;
         double v = sm[j];
         if(v > s || (v == s && j < i)) isMax = false;
         if(v < s || (v == s && j < i)) isMin = false;
        }
      if(isMax && s >= smMax * vHvnPct / 100.0) { int m = ArraySize(hv); ArrayResize(hv, m + 1); hv[m] = i; }
      if(isMin && s <= smMax * vLvnPct / 100.0 && i >= nodeWin && i <= n - 1 - nodeWin) { int m = ArraySize(raw); ArrayResize(raw, m + 1); raw[m] = i; }
     }
   int nh = ArraySize(hv);
   if(nh >= 2)
      for(int q = 0; q < ArraySize(raw); q++)
         if(raw[q] > hv[0] && raw[q] < hv[nh - 1]) { int m = ArraySize(lv); ArrayResize(lv, m + 1); lv[m] = raw[q]; }
  }

//+------------------------------------------------------------------+
//| Advanced analytics (nodes, shape, shifts, relative volume, ...)  |
//+------------------------------------------------------------------+
struct SNode
  {
   int    idx;
   double h;
   int    zl, zh;
  };

void SmoothRows(const int k, double &sm[])
  {
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   int s = MathMax(1, (int)MathFloor(n * vNodeSmoothPct / 100.0 + 0.5));
   ArrayResize(sm, n);
   for(int i = 0; i < n; i++)
     {
      double num = 0, den = 0;
      for(int d = -s; d <= s; d++)
        {
         int j = i + d;
         if(j < 0 || j >= n) continue;
         double w = s + 1 - MathAbs(d);
         num += w * (g_up[s0 + j] + g_dn[s0 + j]);
         den += w;
        }
      sm[i] = num / den;
     }
  }

double PeakProminence(const double &sm[], const int n, const int p, const double eps)
  {
   double h = sm[p], lmin = h, rmin = h;
   for(int L = p - 1; L >= 0 && sm[L] <= h + eps; L--) lmin = MathMin(lmin, sm[L]);
   for(int R = p + 1; R < n && sm[R] <= h + eps; R++) rmin = MathMin(rmin, sm[R]);
   // CRX-023: a side with nothing lower than the peak (peak on the edge of the profile) does not define a col - ignore it
   bool hasL = lmin < h - eps, hasR = rmin < h - eps;
   if(hasL && hasR) return h - MathMax(lmin, rmin);
   if(hasL) return h - lmin;
   if(hasR) return h - rmin;
   return 0.0;
  }

// Advanced nodes: HVN = prominent peaks (plateau -> centre) with a minimum separation; LVN = valley between consecutive HVNs.
// Both get a zone (rows around the extreme that stay within InpNodeZonePct of the peak / within the lowest quarter of the valley depth).
void AdvNodes(const int k, SNode &hv[], SNode &lv[], double &sm[])
  {
   ArrayResize(hv, 0); ArrayResize(lv, 0);
   int n = g_p[k].rowN;
   if(n < 5) { ArrayResize(sm, 0); return; }
   SmoothRows(k, sm);
   double mx = 0;
   for(int i = 0; i < n; i++) mx = MathMax(mx, sm[i]);
   if(mx <= 0) return;
   double eps = mx * 1e-12;
   int peaks[];
   ArrayResize(peaks, 0);
   int i = 0;
   while(i < n)
     {
      int j = i;
      while(j + 1 < n && MathAbs(sm[j + 1] - sm[i]) <= eps) j++;
      bool leftOk = (i == 0) || (sm[i - 1] < sm[i] - eps);
      bool rightOk = (j == n - 1) || (sm[j + 1] < sm[i] - eps);
      if(leftOk && rightOk && !(i == 0 && j == n - 1)) { int m = ArraySize(peaks); ArrayResize(peaks, m + 1); peaks[m] = (i + j) / 2; }
      i = j + 1;
     }
   int cand[];
   ArrayResize(cand, 0);
   for(int q = 0; q < ArraySize(peaks); q++)
     {
      int p = peaks[q];
      if(PeakProminence(sm, n, p, eps) >= vNodeProm / 100.0 * mx - eps && sm[p] >= vHvnPct / 100.0 * mx - eps)
        { int m = ArraySize(cand); ArrayResize(cand, m + 1); cand[m] = p; }
     }
   // strongest first (ties: lower row), keep a peak only if it is far enough from every accepted one
   int nc = ArraySize(cand);
   for(int a = 1; a < nc; a++)
     {
      int v = cand[a], b = a - 1;
      while(b >= 0 && (sm[cand[b]] < sm[v] || (sm[cand[b]] == sm[v] && cand[b] > v))) { cand[b + 1] = cand[b]; b--; }
      cand[b + 1] = v;
     }
   int sep = MathMax(1, (int)MathFloor(n * vNodeSep / 100.0 + 0.5));
   int acc[];
   ArrayResize(acc, 0);
   for(int q = 0; q < nc; q++)
     {
      bool ok = true;
      for(int r = 0; r < ArraySize(acc); r++) if(MathAbs(cand[q] - acc[r]) < sep) { ok = false; break; }
      if(ok) { int m = ArraySize(acc); ArrayResize(acc, m + 1); acc[m] = cand[q]; }
     }
   int na = ArraySize(acc);
   for(int a = 1; a < na; a++) { int v = acc[a], b = a - 1; while(b >= 0 && acc[b] > v) { acc[b + 1] = acc[b]; b--; } acc[b + 1] = v; }
   ArrayResize(hv, na);
   for(int q = 0; q < na; q++)
     {
      int p = acc[q];
      double thr = vNodeZonePct / 100.0 * sm[p];
      int zl = p, zh = p;
      while(zl - 1 >= 0 && sm[zl - 1] >= thr) zl--;
      while(zh + 1 < n && sm[zh + 1] >= thr) zh++;
      hv[q].idx = p; hv[q].h = sm[p]; hv[q].zl = zl; hv[q].zh = zh;
     }
   // CRX-023: neighbouring HVN zones must not overlap - the border is the lowest row between the two peaks
   for(int q = 0; q + 1 < na; q++)
     {
      int a = acc[q], b = acc[q + 1], vb = a;
      for(int x = a; x <= b; x++) if(sm[x] < sm[vb]) vb = x;
      if(hv[q].zh > vb) hv[q].zh = vb;
      if(hv[q + 1].zl <= vb) hv[q + 1].zl = MathMin(vb + 1, hv[q + 1].idx);
      if(hv[q].zh >= hv[q + 1].zl) hv[q].zh = hv[q + 1].zl - 1;
     }
   for(int q = 0; q + 1 < na; q++)
     {
      int a = acc[q], b = acc[q + 1];
      double vmin = sm[a];
      for(int x = a; x <= b; x++) vmin = MathMin(vmin, sm[x]);
      int first = a, last = b;
      for(int x = a; x <= b; x++) if(sm[x] == vmin) { first = x; break; }
      for(int x = b; x >= a; x--) if(sm[x] == vmin) { last = x; break; }
      int v = (first + last) / 2;
      double pk = MathMin(sm[a], sm[b]);
      if(vmin <= vLvnPct / 100.0 * mx + eps && pk - vmin >= vNodeProm / 100.0 * mx - eps)
        {
         double lim = vmin + 0.25 * (pk - vmin);
         int zl = v, zh = v;
         while(zl - 1 > a && sm[zl - 1] <= lim) zl--;
         while(zh + 1 < b && sm[zh + 1] <= lim) zh++;
         int m = ArraySize(lv);
         ArrayResize(lv, m + 1);
         lv[m].idx = v; lv[m].h = vmin; lv[m].zl = zl; lv[m].zh = zh;
        }
     }
  }

double RowSkew(const int k)
  {
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   double tot = 0, m = 0;
   for(int i = 0; i < n; i++) { double t = g_up[s0 + i] + g_dn[s0 + i]; tot += t; m += (i + 0.5) * t; }
   if(tot <= 0) return 0;
   m /= tot;
   double var = 0, c3 = 0;
   for(int i = 0; i < n; i++) { double t = g_up[s0 + i] + g_dn[s0 + i]; double d = (i + 0.5) - m; var += t * d * d; c3 += t * d * d * d; }
   var /= tot;
   if(var <= 1e-18) return 0;
   return c3 / tot / MathPow(MathSqrt(var), 3);
  }

// Heuristic only: D-shape / P-shape / b-shape / double distribution, otherwise Unclassified (never a market-structure claim).
string ShapeOf(const int k)
  {
   int n = g_p[k].rowN;
   if(n < 15 || g_p[k].total <= 0) return "Unclassified (thin)";
   SNode hv[], lv[];
   double sm[];
   AdvNodes(k, hv, lv, sm);
   int K = ArraySize(hv);
   if(K >= 2)
     {
      int b1 = 0, b2 = -1;                                          // the two highest peaks (ties: lower row first)
      for(int q = 1; q < K; q++) if(hv[q].h > hv[b1].h) b1 = q;
      for(int q = 0; q < K; q++) if(q != b1 && (b2 < 0 || hv[q].h > hv[b2].h)) b2 = q;
      int pa = MathMin(hv[b1].idx, hv[b2].idx), pb = MathMax(hv[b1].idx, hv[b2].idx);
      double h1 = (hv[b1].idx == pa) ? hv[b1].h : hv[b2].h, h2 = (hv[b1].idx == pa) ? hv[b2].h : hv[b1].h;
      double valley = sm[pa];
      for(int x = pa; x <= pb; x++) valley = MathMin(valley, sm[x]);
      if((pb - pa) / (double)n >= 0.25 && valley / MathMin(h1, h2) <= 0.6) return "Double distribution";
     }
   double pp = (g_p[k].pocI + 0.5) / n, sk = RowSkew(k);
   if(pp >= 0.62 && sk <= -0.3) return "P-shape";
   if(pp <= 0.38 && sk >= 0.3) return "b-shape";
   if(pp > 0.38 && pp < 0.62 && MathAbs(sk) < 0.3 && K <= 2) return "D-shape";
   return "Unclassified";
  }

string VaShiftText(const double cVal, const double cVah, const double pVal, const double pVah)
  {
   double e = 1e-9;
   if(cVal > pVah + e) return "Higher value";
   if(cVah < pVal - e) return "Lower value";
   if(cVal >= pVal - e && cVah <= pVah + e) return "Inside previous VA";
   if(cVal < pVal - e && cVah > pVah + e) return "Outside previous VA";
   return (cVal + cVah) > (pVal + pVah) ? "Overlapping higher" : "Overlapping lower";
  }

string MigrationState(const double d, const double rs)
  {
   if(MathAbs(d) <= vMigTol * rs + 1e-12) return "Stable";
   return d > 0 ? "Migrating up" : "Migrating down";
  }

// relative volume of profile k: cumulative chart-bar volume now / mean of the previous InpRvolN profiles at the same elapsed bar
// CRX-011: the cumulative series holds ACCEPTED profile volume (intrabars after the session filter, or the chart bar), and the current
// profile is compared with each earlier profile at the SAME ELAPSED TIME since that profile's own first candle - not at the same candle
// ordinal. Earlier profiles that are invalid, of another volume type (real vs tick) or too short to reach that elapsed time are left out.
double RvolOf(const int k, int &used)
  {
   used = 0;
   int kk = g_p[k].cumN;
   if(kk <= 0) return -1;
   double cur = g_cum[g_p[k].cumStart + kk - 1], sum = 0;
   long elapsed = (long)(cTm[g_p[k].i0 + kk - 1] - cTm[g_p[k].i0]);
   for(int q = k - 1; q >= 0 && q >= k - vRvolN; q--)
     {
      if(g_p[q].kind != 0 || !g_p[q].valid || g_p[q].vtype != g_p[k].vtype || g_p[q].cumN <= 0) continue;
      if((long)(cTm[g_p[q].i0 + g_p[q].cumN - 1] - cTm[g_p[q].i0]) < elapsed) continue;      // incomplete / shorter period
      int jj = -1;
      for(int j = 0; j < g_p[q].cumN; j++)
        {
         if((long)(cTm[g_p[q].i0 + j] - cTm[g_p[q].i0]) <= elapsed) jj = j; else break;
        }
      if(jj < 0) continue;
      sum += g_cum[g_p[q].cumStart + jj];
      used++;
     }
   if(used < 2) return -1;
   double a = sum / used;
   return a > 0 ? cur / a : -1;
  }

// clusters of consecutive sorted levels whose neighbour gap <= tol; keeps clusters with at least minc levels
int ClusterLevels(const double &pr[], const string &nm[], const string &sk[], const int cnt, const double tol, const int minc, double &zlo[], double &zhi[], int &zn[], string &zs[])
  {
   // CRX-022: a zone is a window of at most `tol` price width (no chaining), and it counts DISTINCT sources
   // (profile / composite / anchored) - POC+VAH+VAL of one profile are one source, not three confirmations
   int idx[];
   ArrayResize(idx, cnt);
   for(int i = 0; i < cnt; i++) idx[i] = i;
   for(int a = 1; a < cnt; a++) { int v = idx[a], b = a - 1; while(b >= 0 && pr[idx[b]] > pr[v]) { idx[b + 1] = idx[b]; b--; } idx[b + 1] = v; }
   ArrayResize(zlo, 0); ArrayResize(zhi, 0); ArrayResize(zn, 0); ArrayResize(zs, 0);
   int start = 0;
   while(start < cnt)
     {
      int end = start;
      while(end + 1 < cnt && pr[idx[end + 1]] - pr[idx[start]] <= tol + 1e-12) end++;
      string seen = "|";
      int dist = 0;
      string lab = "";
      for(int x = start; x <= end; x++)
        {
         string key = "|" + sk[idx[x]] + "|";
         if(StringFind(seen, key) >= 0) continue;
         seen += sk[idx[x]] + "|";
         dist++;
         if(dist <= 4) lab += (dist > 1 ? "+" : "") + nm[idx[x]];
        }
      if(dist >= minc)
        {
         int m = ArraySize(zlo);
         ArrayResize(zlo, m + 1); ArrayResize(zhi, m + 1); ArrayResize(zn, m + 1); ArrayResize(zs, m + 1);
         zlo[m] = pr[idx[start]]; zhi[m] = pr[idx[end]]; zn[m] = dist; zs[m] = lab;
         start = end + 1;
        }
      else start++;
     }
   return ArraySize(zlo);
  }

//+------------------------------------------------------------------+
//| Composite: resample finished profiles onto a common grid          |
//+------------------------------------------------------------------+
// Every source row keeps its volume spread uniformly over its own price span; the composite rows take the overlapping share.
// Volume is conserved exactly (shares add up to 1), so nothing is counted twice.
int MakeComposite(const int &src[], const int ns, const int rowsWanted)
  {
   if(ns <= 0) return -1;
   double lo = 1e300, hi = -1e300;
   for(int q = 0; q < ns; q++)
     {
      int p = src[q];
      lo = MathMin(lo, g_p[p].off + g_p[p].base * g_p[p].rs);
      hi = MathMax(hi, g_p[p].off + (g_p[p].base + g_p[p].rowN) * g_p[p].rs);
     }
   double rs = MathMax(g_tick, MathRound((hi - lo) / MathMax(1, rowsWanted) / g_tick) * g_tick);   // CRX-002/CRX-014: tick-aligned
   int n = (int)MathMax(1.0, MathCeil((hi - lo) / rs - 1e-9));
   int k = ArraySize(g_p);
   ArrayResize(g_p, k + 1);
   int s0 = ArraySize(g_up);
   ArrayResize(g_up, s0 + n); ArrayResize(g_dn, s0 + n);
   for(int x = 0; x < n; x++) { g_up[s0 + x] = 0.0; g_dn[s0 + x] = 0.0; }
   double tot = 0, upT = 0, mnLo = 1e300, mxHi = -1e300;
   datetime t0 = g_p[src[0]].t0, t1 = g_p[src[0]].t1;
   int i0 = g_p[src[0]].i0, i1 = g_p[src[0]].i1, vt = 1;
   for(int q = 0; q < ns; q++)
     {
      int p = src[q];
      for(int i = 0; i < g_p[p].rowN; i++)
        {
         double vu = g_up[g_p[p].rowStart + i], vd = g_dn[g_p[p].rowStart + i];
         if(vu <= 0 && vd <= 0) continue;
         double b = g_p[p].off + (g_p[p].base + i) * g_p[p].rs, tp = b + g_p[p].rs;
         int j0 = (int)MathMax(0.0, MathFloor((b - lo) / rs + 1e-9));
         int j1 = (int)MathMin((double)(n - 1), MathFloor((tp - lo) / rs - 1e-9));
         if(j1 < j0) j1 = j0;
         double S = 0;
         for(int j = j0; j <= j1; j++) S += MathMax(0.0, MathMin(tp, lo + (j + 1) * rs) - MathMax(b, lo + j * rs));
         for(int j = j0; j <= j1; j++)
           {
            double ov = MathMax(0.0, MathMin(tp, lo + (j + 1) * rs) - MathMax(b, lo + j * rs));
            double f = S > 0 ? ov / S : (j == j0 ? 1.0 : 0.0);
            g_up[s0 + j] += vu * f; g_dn[s0 + j] += vd * f;
           }
        }
      tot += g_p[p].total; upT += g_p[p].upTotal;
      mnLo = MathMin(mnLo, g_p[p].lo); mxHi = MathMax(mxHi, g_p[p].hi);
      if(g_p[p].t0 < t0) { t0 = g_p[p].t0; i0 = g_p[p].i0; }
      if(g_p[p].t1 > t1) { t1 = g_p[p].t1; i1 = g_p[p].i1; }
      if(g_p[p].vtype == 0) vt = 0;
     }
   g_p[k].i0 = i0; g_p[k].i1 = i1; g_p[k].t0 = t0; g_p[k].t1 = t1;
   g_p[k].rs = rs; g_p[k].off = lo; g_p[k].base = 0; g_p[k].rowStart = s0; g_p[k].rowN = n;
   g_p[k].total = tot; g_p[k].upTotal = upT; g_p[k].hi = mxHi; g_p[k].lo = mnLo;
   g_p[k].poc = EMPTY_VALUE; g_p[k].vah = EMPTY_VALUE; g_p[k].val = EMPTY_VALUE;
   g_p[k].vaCov = 0; g_p[k].vtype = vt; g_p[k].valid = true; g_p[k].naked = false; g_p[k].prov = false; g_p[k].touchBar = -1;
   g_p[k].kind = 1; g_p[k].slot = 0; g_p[k].cumStart = 0; g_p[k].cumN = 0; g_p[k].ibH = EMPTY_VALUE; g_p[k].ibL = EMPTY_VALUE; g_p[k].ibEst = false;
   g_p[k].qIntra = 0; g_p[k].qFb = 0; g_p[k].qSkip = 0; g_p[k].qExp = 0; g_p[k].qGot = 0; g_p[k].qSparse = 0; g_p[k].qPartial = 0; g_p[k].qTypeMis = 0; g_p[k].qVolIn = 0; g_p[k].qVolChart = 0;
   for(int q = 0; q < ns; q++)
     {
      int p = src[q];
      g_p[k].qIntra += g_p[p].qIntra; g_p[k].qFb += g_p[p].qFb; g_p[k].qSkip += g_p[p].qSkip; g_p[k].qExp += g_p[p].qExp; g_p[k].qGot += g_p[p].qGot;
      g_p[k].qSparse += g_p[p].qSparse; g_p[k].qPartial += g_p[p].qPartial; g_p[k].qTypeMis += g_p[p].qTypeMis; g_p[k].qVolIn += g_p[p].qVolIn; g_p[k].qVolChart += g_p[p].qVolChart;
     }
   g_p[k].bPrecise = 0; g_p[k].bEstimated = 0; g_p[k].bExcluded = 0; g_p[k].clamped = 0;
   SetLevels(k);
   double rowSum = 0;
   for(int x = 0; x < n; x++) rowSum += g_up[s0 + x] + g_dn[s0 + x];
   if(MathAbs(rowSum - tot) > 1e-9 * MathMax(1.0, tot)) g_p[k].valid = false;
   return k;
  }

// first CLOSED chart bar after the profile that revisits its POC (wick touch, or a close through the level); -1 = still naked.
// The forming bar (N-1) is never used here, so the confirmed state cannot flip back after a reload.
int FirstTouch(const int k)
  {
   double poc = g_p[k].poc;
   if(poc == EMPTY_VALUE) return -1;
   double side = (cCl[g_p[k].i1] - poc) >= 0 ? 1.0 : -1.0;
   for(int i = g_p[k].i1 + 1; i < N - 1; i++)
     {
      if(InpNakedTouch == NT_WICK) { if(cLo[i] <= poc && cHi[i] >= poc) return i; }
      else if((cCl[i] - poc) * side <= 0) return i;
     }
   return -1;
  }

//+------------------------------------------------------------------+
//| Build all profiles                                               |
//+------------------------------------------------------------------+
int LtfLB(const datetime t)                              // first lower-timeframe candle with time >= t
  {
   int lo = 0, hi = lN;
   while(lo < hi) { int mid = (lo + hi) / 2; if(lT[mid] < t) lo = mid + 1; else hi = mid; }
   return lo;
  }

int ChartLB(const datetime t)                            // first chart bar with time >= t
  {
   int lo = 0, hi = N;
   while(lo < hi) { int mid = (lo + hi) / 2; if(cTm[mid] < t) lo = mid + 1; else hi = mid; }
   return lo;
  }

// periods (chronological) of the chart bars up to 'last', newest 'want' of them.
// CRX-001: bb[k] is the chart bar just BEFORE block k (index p0[k]-1) when that bar's own OPEN time is outside the
// period but its LAST instant (open + chartSec - 1) is already inside it — i.e. a candle that straddles the boundary
// and would otherwise be silently dropped in full (its in-period tail is real data, not noise).
double LocalMinuteOfDay(const datetime t, const int filt);

// CRX-028: period id of candle i. Normally the id of its open time. A session SHORTER than the chart bar can lie entirely
// inside one candle (e.g. H4 08:00-12:00 holding session 08:30-10:00): then no candle opens inside the session and the
// session was lost. In that case the id of the session start instant inside the candle is returned, so the candle becomes
// a (boundary) candle of that session. ff: 0 = primary (only in session mode), 1..4 = session slot.
long CandIdS(const int i, const int ff)
  {
   long id = IdOf(cTm[i], ff);
   if(id >= 0 || g_chartSec <= 60) return id;
   if(ff == 0 && g_effPeriod != VP_SESSION) return id;
   double sSm = (ff == 0) ? (double)g_sessS : (double)g_slotS[ff - 1];
   double dm = sSm - LocalMinuteOfDay(cTm[i], ff);
   if(dm < 0) dm += 1440.0;
   if(dm * 60.0 >= g_chartSec) return id;                                      // the session does not start inside this candle
   long pid2 = IdOf(cTm[i] + (datetime)MathRound(dm * 60.0), ff);
   if(pid2 < 0) return id;
   if(i + 1 < N && cTm[i + 1] == cTm[i] + (datetime)g_chartSec && IdOf(cTm[i + 1], ff) == pid2) return id;   // continues into the next candle: that one owns it (boundary-before)
   return pid2;
  }

int SplitPeriods(const int filt, const int want, const int last, int &p0[], int &p1[], int &bb[])
  {
   ArrayResize(p0, 0); ArrayResize(p1, 0); ArrayResize(bb, 0);
   int ff = (filt == 100) ? 0 : filt;                     // id function: filt 100 = primary session mode (still PeriodId, ff=0)
   int i = last;
   while(i >= 0 && ArraySize(p0) < want)
     {
      long id = CandIdS(i, ff);
      if(id < 0) { i--; continue; }
      int e = i;
      while(i - 1 >= 0 && IdOf(cTm[i - 1], ff) == id) i--;
      int m = ArraySize(p0);
      ArrayResize(p0, m + 1); ArrayResize(p1, m + 1); ArrayResize(bb, m + 1);
      p0[m] = i; p1[m] = e;
      bb[m] = -1;
      if(filt != 0 && i - 1 >= 0 && IdOf(cTm[i - 1] + (datetime)g_chartSec - 1, ff) == id) bb[m] = i - 1;
      i--;
     }
   int n = ArraySize(p0);
   for(int a = 0; a < n / 2; a++)
     {
      int t0 = p0[a], t1 = p1[a], tb = bb[a];
      p0[a] = p0[n - 1 - a]; p1[a] = p1[n - 1 - a]; bb[a] = bb[n - 1 - a];
      p0[n - 1 - a] = t0; p1[n - 1 - a] = t1; bb[n - 1 - a] = tb;
     }
   return n;
  }

// local minute-of-day of a server timestamp, under the time basis that IdOf(.., filt) itself uses (primary InpTz, or the slot's own zone)
double LocalMinuteOfDay(const datetime t, const int filt)
  {
   datetime tl = (filt >= 1 && filt <= 4) ? ToLocalTz(t, SlotTz(filt - 1)) : ToLocal(t);
   MqlDateTime d;
   TimeToStruct(tl, d);
   return d.hour * 60 + d.min + d.sec / 60.0;
  }

double OvRange(const double m0, const double m1, const double lo, const double hi)
  {
   double x = MathMin(m1, hi) - MathMax(m0, lo);
   return x > 0 ? x : 0.0;
  }

// CRX-001: share of chart bar [t, t+chartSec) that falls inside the session [sS,sE) (minutes of day, sS>sE = overnight wrap).
// Assumes no DST jump strictly inside this one bar (documented approximation, see CRX-013 / Known Limitations).
double OverlapFraction(const datetime t, const int filt, const double sS, const double sE)
  {
   if(filt == 200) return MathMin(1.0, MathMax(0.0, (double)((long)t + g_chartSec - (long)InpAncTime) / g_chartSec));
   double m0 = LocalMinuteOfDay(t, filt);
   double m1 = m0 + g_chartSec / 60.0;
   double covered;
   if(sS < sE) covered = OvRange(m0, m1, sS, sE);
   else covered = MathMax(OvRange(m0, m1, sS, 1440) + OvRange(m0, m1, 0, sE), MathMax(OvRange(m0, m1, sS - 1440, sE), OvRange(m0, m1, sS, sE + 1440)));
   double frac = covered / (g_chartSec / 60.0);
   return MathMin(1.0, MathMax(0.0, frac));
  }

// true when candle i is only PARTIALLY inside the period 'pid' (its open or its last instant falls outside it) — only
// meaningful when useFilter; such a candle's full H/L/volume must never be added blindly (CRX-001).
bool IsBoundaryCandle(const int i, const int filt, const long pid)
  {
   bool openIn = IdOf(cTm[i], filt) == pid;
   bool closeIn = IdOf(cTm[i] + (datetime)g_chartSec - 1, filt) == pid;
   return !(openIn && closeIn);
  }

// builds ONE profile from chart bars a..b and appends it. kind: 0 primary, 2 anchored, 3 session slot.
// filt: 0 = no filter, 100 = primary session mode (id == pid), 1..4 = session slot (id == pid).
// boundaryBefore >= 0: an extra candle just before 'a' whose tail overlaps the period (see SplitPeriods / CRX-001).
// sS/sE: session minutes-of-day, only used when filt != 0 (for the boundary-candle overlap fraction).
// rowsExact > 0: grid fitted to the exact range (rows = rowsExact), otherwise the row size comes from 'est' (typical range) / rows.
int BuildOne(const int a, const int b, const int kind, const int slot, const int filt, const long pid,
             const double est, const int rowsExact, const bool dev, const int boundaryBefore = -1,
             const double sS = 0, const double sE = 0)
  {
   int k = ArraySize(g_p);
   ArrayResize(g_p, k + 1);
   int vt = ChooseVolType(a, b);
   int ff = (filt == 100) ? 0 : filt;                    // id function: primary period or slot
   bool useFilter = (filt != 0);
   bool headB = (filt == 200 && cTm[a] < InpAncTime);              // CRX-016: the anchor falls INSIDE the first candle - only its intrabars from the anchor on belong to the profile
   double hi = cHi[a], lo = cLo[a];
   if(headB && a < b) { hi = cHi[a + 1]; lo = cLo[a + 1]; }
   for(int i = headB ? a + 1 : a; i <= b; i++) { if(cHi[i] > hi) hi = cHi[i]; if(cLo[i] < lo) lo = cLo[i]; }
   if(g_useLtf)                                          // intrabar extremes too, so that every candle lies inside the grid
     {
      datetime tS = cTm[a], tE = cTm[b] + g_chartSec;
      for(int j = LtfLB(tS); j < lN && lT[j] < tE; j++)
        {
         if(useFilter && IdOf(lT[j], ff) != pid) continue;
         if(lH[j] > hi) hi = lH[j];
         if(lL[j] < lo) lo = lL[j];
        }
     }
   if(headB && InpSessBound == SESSB_ESTIMATE && InpVolSrc != SRC_STRICT) { if(cHi[a] > hi) hi = cHi[a]; if(cLo[a] < lo) lo = cLo[a]; }   // the estimate (if it is needed) uses the whole candle's range
   // CRX-001: the boundary-before candle's own H/L must never widen the grid wholesale (it is only partly in the
   // period). Use its filtered intrabars when they exist; otherwise only the Estimate policy borrows the full candle.
   int bbJ0 = 0, bbJ1 = 0;
   bool bbUseIntra = false;
   if(boundaryBefore >= 0)
     {
      bbJ0 = LtfLB(cTm[boundaryBefore]); bbJ1 = LtfLB(cTm[boundaryBefore] + g_chartSec);
      if(bbJ1 > bbJ0)
        {
         for(int j = bbJ0; j < bbJ1; j++) if(IdOf(lT[j], ff) == pid) { if(lH[j] > hi) hi = lH[j]; if(lL[j] < lo) lo = lL[j]; }
         if(InpVolSrc == SRC_STRICT) bbUseIntra = true;
         else
           {
            double bbSumL = 0;
            for(int j = bbJ0; j < bbJ1; j++) bbSumL += (vt == 1 ? lVr[j] : lVt[j]);
            double bbCv = ChartVol(boundaryBefore, vt);
            bbUseIntra = (bbCv <= 0 && bbSumL <= 0) || (bbCv > 0 && MathAbs(bbSumL - bbCv) <= bbCv * vVolTol / 100.0);
           }
         if(!bbUseIntra && InpSessBound == SESSB_ESTIMATE)              // the estimated add below will use the full candle's range too
           {
            if(cHi[boundaryBefore] > hi) hi = cHi[boundaryBefore];
            if(cLo[boundaryBefore] < lo) lo = cLo[boundaryBefore];
           }
        }
      else if(InpSessBound == SESSB_ESTIMATE)
        {
         if(cHi[boundaryBefore] > hi) hi = cHi[boundaryBefore];
         if(cLo[boundaryBefore] < lo) lo = cLo[boundaryBefore];
        }
     }
   double tick = g_tick, rs, off = 0.0;
   if(rowsExact > 0)
     {
      rs = MathMax(tick, MathRound((hi - lo) / rowsExact / tick) * tick);    // CRX-002/CRX-014: row size always a whole multiple of the tick size
      off = lo;
     }
   else
     {
      if(InpRowMode == ROWS_FIXED) rs = vRowTicks * tick;
      else rs = MathMax(tick, MathRound(est / MathMax(1, vRows) / tick) * tick);
      rs = MathMax(rs, MathCeil(est / 1500.0 / tick) * tick);       // safety cap: at most ~1500 rows from the estimate
     }
   if(rs <= 0) rs = tick;
   int base = (int)MathFloor((lo - off) / rs + 1e-9);
   int top = (int)MathCeil((hi - off) / rs - 1e-9) - 1;
   if(top < base) top = base;
   if(top - base + 1 > 2000)                             // too many rows: coarser grid BEFORE any volume is added (volume stays conserved)
     {
      rs = MathMax(tick, MathCeil((hi - lo) / 1999.0 / tick) * tick);                // CRX-002/CRX-014: tick-aligned, rounded UP so the cap really holds
      base = (int)MathFloor((lo - off) / rs + 1e-9);
      top = (int)MathCeil((hi - off) / rs - 1e-9) - 1;
      if(top < base) top = base;
      while(top - base + 1 > 2000) { rs += tick; base = (int)MathFloor((lo - off) / rs + 1e-9); top = (int)MathCeil((hi - off) / rs - 1e-9) - 1; if(top < base) top = base; }
     }
   int rowN = top - base + 1;
   int s0 = ArraySize(g_up);
   ArrayResize(g_up, s0 + rowN); ArrayResize(g_dn, s0 + rowN);
   for(int q = 0; q < rowN; q++) { g_up[s0 + q] = 0.0; g_dn[s0 + q] = 0.0; }
   int c0 = ArraySize(g_cum);
   ArrayResize(g_cum, c0 + (b - a + 1));
   g_p[k].i0 = a; g_p[k].i1 = b; g_p[k].t0 = cTm[a]; g_p[k].t1 = cTm[b];
   g_p[k].rs = rs; g_p[k].off = off; g_p[k].base = base; g_p[k].rowStart = s0; g_p[k].rowN = rowN;
   g_p[k].total = 0; g_p[k].upTotal = 0; g_p[k].hi = hi; g_p[k].lo = lo;
   g_p[k].poc = EMPTY_VALUE; g_p[k].vah = EMPTY_VALUE; g_p[k].val = EMPTY_VALUE;
   g_p[k].vaCov = 0; g_p[k].vtype = vt; g_p[k].valid = true; g_p[k].naked = true; g_p[k].prov = false; g_p[k].touchBar = -1;
   g_p[k].kind = kind; g_p[k].slot = slot; g_p[k].cumStart = c0; g_p[k].cumN = b - a + 1;
   g_p[k].ibH = EMPTY_VALUE; g_p[k].ibL = EMPTY_VALUE; g_p[k].ibEst = false;
   g_p[k].qIntra = 0; g_p[k].qFb = 0; g_p[k].qSkip = 0; g_p[k].qExp = 0; g_p[k].qGot = 0; g_p[k].qSparse = 0; g_p[k].qPartial = 0; g_p[k].qTypeMis = 0; g_p[k].qVolIn = 0; g_p[k].qVolChart = 0;
   g_p[k].bPrecise = 0; g_p[k].bEstimated = 0; g_p[k].bExcluded = 0; g_p[k].clamped = 0;
   if(kind == 0) { if(vt == 1) g_nReal++; else g_nTick++; }

   // CRX-001: the boundary-before candle (its own open time is outside the period, but its tail is in it).
   // Fully precise when intrabars exist (they are filtered to the in-period minutes, same as any candle below);
   // otherwise the chosen policy decides: silently add nothing (Exclude) or a time-prorated, clearly counted estimate.
   if(boundaryBefore >= 0)
     {
      if(bbUseIntra)
        {
         for(int j = bbJ0; j < bbJ1; j++) if(IdOf(lT[j], ff) == pid) AddVol(k, lH[j], lL[j], vt == 1 ? lVr[j] : lVt[j], lU[j]);
         g_p[k].bPrecise++;
        }
      else if(InpVolSrc == SRC_STRICT)
         g_p[k].bExcluded++;                                         // Strict: no usable intrabars for the boundary candle (none at all, or bbUseIntra stayed false) -> nothing added
      else if(InpSessBound == SESSB_ESTIMATE)
        {
         double bbFrac = OverlapFraction(cTm[boundaryBefore], filt, sS, sE);
         AddVol(k, cHi[boundaryBefore], cLo[boundaryBefore], ChartVol(boundaryBefore, vt) * bbFrac, cCl[boundaryBefore] >= cOp[boundaryBefore]);
         g_p[k].bEstimated++;
        }
      else
         g_p[k].bExcluded++;
     }

   double cum = 0;
   for(int i = a; i <= b; i++)
     {
      bool isBoundary = useFilter && IsBoundaryCandle(i, ff, pid);      // only the LAST candle of the block can be one (see SplitPeriods)
      datetime tb0 = cTm[i], tb1 = cTm[i] + g_chartSec;
      int j0 = LtfLB(tb0), j1 = LtfLB(tb1);
      double sumL = 0;
      for(int j = j0; j < j1; j++) sumL += (vt == 1 ? lVr[j] : lVt[j]);
      bool useIntra = false;
      if(g_useLtf && j1 > j0)
        {
         double cv = ChartVol(i, vt);                       // intrabars must add up to the chart bar, never both, never rescaled
         bool volMatch = (cv <= 0 && sumL <= 0) || (cv > 0 && MathAbs(sumL - cv) <= cv * vVolTol / 100.0);
         if(InpVolSrc == SRC_STRICT) useIntra = true;
         else useIntra = volMatch;
         if(!isBoundary)
           {
            // CRX-008/017: time coverage = LTF slots present / slots expected; volume coverage = intrabar volume / chart-bar volume (per profile; globals only for primary profiles).
            // A candle with missing slots whose volume still matches is treated as "minutes without trades", not as lost data.
            int expN = (int)MathMax(1, g_chartSec / MathMax(1, PeriodSeconds(g_ltf)));
            long gotN = MathMin(expN, j1 - j0);
            g_p[k].qExp += expN; g_p[k].qGot += gotN;
            if(volMatch && j1 - j0 < expN) g_p[k].qSparse++;
            if(!volMatch && InpVolSrc == SRC_STRICT) g_p[k].qPartial++;
            if(cv > 0 && sumL <= 0) g_p[k].qTypeMis++;
            if(cv > 0) { g_p[k].qVolChart += cv; g_p[k].qVolIn += MathMin(cv, sumL); }
            if(kind == 0)
              {
               g_cvExp += expN; g_cvGot += gotN;
               if(volMatch && j1 - j0 < expN) g_cvSparse++;
               if(!volMatch && InpVolSrc == SRC_STRICT) g_cvPartial++;
               if(cv > 0 && sumL <= 0) g_cvTypeMis++;
               if(cv > 0) { g_cvVolChart += cv; g_cvVolIn += MathMin(cv, sumL); }
              }
           }
        }
      double addedVol = 0.0;
      if(useIntra)
        {
         for(int j = j0; j < j1; j++)
           {
            if(useFilter && IdOf(lT[j], ff) != pid) continue;                // intrabars outside the session do not belong to it
            double vv = vt == 1 ? lVr[j] : lVt[j];
            AddVol(k, lH[j], lL[j], vv, lU[j]);
            addedVol += vv;
           }
         g_p[k].qIntra++;
         if(kind == 0) g_ltfBars++;
         if(isBoundary) g_p[k].bPrecise++;
        }
      else if(InpVolSrc == SRC_STRICT && g_useLtf)
        {
         g_p[k].qSkip++;
         if(kind == 0) g_skipBars++;
         if(isBoundary) g_p[k].bExcluded++;
        }
      else if(!isBoundary)
        {
         addedVol = ChartVol(i, vt);
         AddVol(k, cHi[i], cLo[i], addedVol, cCl[i] >= cOp[i]);
         g_p[k].qFb++;
         if(kind == 0) g_fbBars++;
        }
      else if(InpSessBound == SESSB_ESTIMATE)                             // boundary candle, no usable intrabars: prorate, never blind-add
        {
         double frac = OverlapFraction(cTm[i], filt, sS, sE);
         addedVol = ChartVol(i, vt) * frac;
         AddVol(k, cHi[i], cLo[i], addedVol, cCl[i] >= cOp[i]);
         g_p[k].bEstimated++;
        }
      else
         g_p[k].bExcluded++;                                              // Exclude policy (default): contributes nothing, counted
      cum += addedVol;
      g_cum[c0 + (i - a)] = cum;
      if(dev)
        {
         int lo2, hi2;
         int pI = CalcVA(k, lo2, hi2);
         if(pI >= 0)
           {
            BufPoc[g_bufOff + i] = g_p[k].off + (g_p[k].base + pI + 0.5) * g_p[k].rs;
            BufVah[g_bufOff + i] = g_p[k].off + (g_p[k].base + hi2 + 1) * g_p[k].rs;
            BufVal[g_bufOff + i] = g_p[k].off + (g_p[k].base + lo2) * g_p[k].rs;
           }
        }
     }
   if(kind == 0)                                         // Initial Balance: the first vIBMin minutes counted from the REAL period start
     {
      // CRX-012: in Session mode the window starts at the session's own start time (which can fall inside a candle), not at the first chart bar;
      // the window is [ibS, ibE). Candles fully inside use their H/L; partly-covered candles use only the intrabars inside the window,
      // and are left out (IB flagged approximate) when no intrabars exist. Overnight sessions work because the start is derived per profile.
      datetime ibS = cTm[a];
      int ib0 = a;
      if(useFilter && boundaryBefore >= 0)
        {
         double m0 = LocalMinuteOfDay(cTm[boundaryBefore], filt);
         double x = MathMod(sS - m0 + 2880.0, 1440.0);
         if(x > 0 && x < g_chartSec / 60.0) { ibS = cTm[boundaryBefore] + (datetime)MathRound(x * 60.0); ib0 = boundaryBefore; }
        }
      datetime ibE = ibS + (datetime)(vIBMin * 60);
      for(int i = ib0; i <= b && cTm[i] < ibE; i++)
        {
         datetime c0t = cTm[i], c1t = cTm[i] + g_chartSec;
         double h2 = EMPTY_VALUE, l2 = EMPTY_VALUE;
         if(c0t >= ibS && c1t <= ibE) { h2 = cHi[i]; l2 = cLo[i]; }
         else
           {
            int jA = LtfLB(c0t), jB = LtfLB(c1t);
            for(int j = jA; j < jB; j++)
              {
               if(lT[j] < ibS || lT[j] >= ibE) continue;
               if(useFilter && IdOf(lT[j], ff) != pid) continue;
               if(h2 == EMPTY_VALUE || lH[j] > h2) h2 = lH[j];
               if(l2 == EMPTY_VALUE || lL[j] < l2) l2 = lL[j];
              }
            if(h2 == EMPTY_VALUE && c1t > ibS && c0t < ibE) g_p[k].ibEst = true;      // partly inside the window, nothing to measure it with
           }
         if(h2 == EMPTY_VALUE) continue;
         if(g_p[k].ibH == EMPTY_VALUE || h2 > g_p[k].ibH) g_p[k].ibH = h2;
         if(g_p[k].ibL == EMPTY_VALUE || l2 < g_p[k].ibL) g_p[k].ibL = l2;
        }
     }
   SetLevels(k);
   double rowSum = 0;
   for(int q = 0; q < rowN; q++) rowSum += g_up[s0 + q] + g_dn[s0 + q];
   // CRX-003: conservation of the SUM alone is not enough - any row the data had to be clamped into makes the
   // DISTRIBUTION untrustworthy too (its price location is wrong), so the whole profile is marked invalid.
   if(MathAbs(rowSum - g_p[k].total) > 1e-9 * MathMax(1.0, g_p[k].total) || g_p[k].upTotal > g_p[k].total + 1e-9 * MathMax(1.0, g_p[k].total) || g_p[k].clamped > 0) g_p[k].valid = false;
   if(InpVolType == VT_REAL && vt == 1 && g_p[k].total <= 0) { g_p[k].valid = false; g_realEmpty++; }   // CRX-010: Real-only but the broker delivers no real volume
   if(g_strictNA) g_p[k].valid = false;                                  // CRX-009: Strict never silently falls back to chart bars
   return k;
  }

double RangeEstimate(const int a, const double fallbackRange)       // bootstrap: 3 x the average candle range before bar a
  {
   int c0 = MathMax(1, a - 20), cnt = 0;
   double e = 0;
   for(int q = c0; q <= a && q < N; q++) { e += cHi[q] - cLo[q]; cnt++; }
   return (cnt > 0 ? e / cnt : fallbackRange) * 3.0;
  }

void BuildProfiles()
  {
   g_np = 0; g_tot = 0;
   ArrayResize(g_p, 0); ArrayResize(g_up, 0); ArrayResize(g_dn, 0); ArrayResize(g_cum, 0);
   ArrayInitialize(BufPoc, EMPTY_VALUE); ArrayInitialize(BufVah, EMPTY_VALUE); ArrayInitialize(BufVal, EMPTY_VALUE);
   g_curIdx = -1; g_doneIdx = -1; g_clamped = 0; g_compMixEx = 0; g_compCut = 0; g_compN = 0; g_realEmpty = 0; g_nReal = 0; g_nTick = 0; g_compIdx = -1; g_ancIdx = -1;
   for(int s = 0; s < 4; s++) { g_msA[s] = -1; g_msB[s] = -1; g_msRun[s] = false; }
   int last = N - 2;                                     // closed bars only
   if(last < 2) return;
   int wantP = vKeepPrev + 1 + (InpNaked ? vNakedLook : 0);
   if(InpRvol) wantP = MathMax(wantP, vRvolN + 2);
   if(InpComp) wantP = MathMax(wantP, InpCompMode == COMP_LASTN ? vCompN + 1 : 40);

   //---- 1) period lists: primary, anchored start, session slots
   int pi0[], pi1[], pib[];
   ArrayResize(pi0, 0); ArrayResize(pi1, 0); ArrayResize(pib, 0);
   if(g_effPeriod == VP_ROLL)
     {
      ArrayResize(pi0, 1); ArrayResize(pi1, 1); ArrayResize(pib, 1);
      pi0[0] = MathMax(0, last - vRollN + 1); pi1[0] = last; pib[0] = -1;
     }
   else SplitPeriods(g_effPeriod == VP_SESSION ? 100 : 0, wantP, last, pi0, pi1, pib);
   int np = ArraySize(pi0);
   int ancA = -1;
   g_ancState = 0; g_ancLostSec = 0;
   if(InpAnc && InpAncTime > 0)
     {
      int c0a = ChartLB(InpAncTime);                           // first candle opening at/after the anchor
      if(c0a > last && !(last >= 0 && InpAncTime < cTm[last] + g_chartSec)) { ancA = -1; g_ancState = 5; }     // anchor lies after the newest candle
      else if(c0a == 0 && InpAncTime < cTm[0]) { ancA = 0; g_ancState = 4; }                                  // before the available history
      else if(c0a <= last && cTm[c0a] == InpAncTime) { ancA = c0a; g_ancState = 1; }                         // exactly on a candle open
      else
        {
         int cc = c0a - 1;                                      // candle that contains / precedes the anchor
         bool inside = cc >= 0 && InpAncTime < cTm[cc] + g_chartSec;
         if(!inside) { ancA = c0a; g_ancState = 2; }            // anchor in a gap between candles (market closed): next candle, nothing lost
         else if(g_useLtf) { ancA = cc; g_ancState = 6; }       // mid-candle, intrabars cut it exactly at the anchor
         else { ancA = c0a; g_ancState = 3; g_ancLostSec = (int)(cTm[cc] + g_chartSec - InpAncTime); }   // mid-candle, no intrabars: snapped forward
         if(ancA > last) { ancA = -1; g_ancState = 5; }
        }
     }
   int sp0[4][2], sp1[4][2], spb[4][2], spn[4];
   for(int s = 0; s < 4; s++)
     {
      spn[s] = 0;
      if(!SlotOn(s)) continue;
      int t0[], t1[], tb[];
      int n2 = SplitPeriods(s + 1, 2, last, t0, t1, tb);
      spn[s] = n2;
      for(int q = 0; q < n2; q++) { sp0[s][q] = t0[q]; sp1[s][q] = t1[q]; spb[s][q] = tb[q]; }
     }
   if(np == 0 && ancA < 0 && spn[0] + spn[1] + spn[2] + spn[3] == 0) return;

   //---- 2) lower-timeframe data once for the earliest start of anything that needs it
   int from = last;
   if(np > 0) from = MathMin(from, pib[0] >= 0 ? pib[0] : pi0[0]);   // CRX-001: also load the oldest profile's boundary-before candle
   if(ancA >= 0) from = MathMin(from, ancA);
   for(int s = 0; s < 4; s++) if(spn[s] > 0) from = MathMin(from, spb[s][0] >= 0 ? spb[s][0] : sp0[s][0]);
   LoadLtf(cTm[from], cTm[last] + g_chartSec);

   //---- 3) primary profiles
   int dispFrom = MathMax(0, np - (vKeepPrev + 1));
   double hist[5];
   int histN = 0;
   for(int k = 0; k < np; k++)
     {
      int a = pi0[k], b = pi1[k];
      long pid = (g_effPeriod == VP_ROLL) ? -2 : CandIdS(a, 0);
      double est = 0;
      if(g_effPeriod != VP_ROLL)
        {
         if(histN > 0) { for(int q = 0; q < histN; q++) est += hist[q]; est /= histN; }
         else { double h0 = cHi[a], l0 = cLo[a]; for(int i = a; i <= b; i++) { if(cHi[i] > h0) h0 = cHi[i]; if(cLo[i] < l0) l0 = cLo[i]; } est = RangeEstimate(a, h0 - l0); }
        }
      int idx = BuildOne(a, b, 0, 0, g_effPeriod == VP_SESSION ? 100 : 0, pid, est, (g_effPeriod == VP_ROLL || InpRowMode == ROWS_COUNT) ? vRows : 0,
                         InpShowDev && g_effPeriod != VP_ROLL && k >= dispFrom, pib[k], g_sessS, g_sessE);
      if(histN == 5) { for(int q = 0; q < 4; q++) hist[q] = hist[q + 1]; histN = 4; }
      hist[histN++] = g_p[idx].hi - g_p[idx].lo;
     }
   g_np = np;

   //---- 4) current vs finished: a profile is "current" only if the FORMING bar belongs to the same period
   if(np > 0)
     {
      if(g_effPeriod == VP_ROLL) { g_curIdx = 0; g_doneIdx = -1; }
      else
        {
         long formingId = CandIdS(N - 1, 0);
         long lastId = CandIdS(g_p[np - 1].i1, 0);
         if(formingId >= 0 && formingId == lastId) { g_curIdx = np - 1; g_doneIdx = np - 2; }
         else { g_curIdx = -1; g_doneIdx = np - 1; }
        }
     }
   //---- 5) naked POC lifecycle (first revisit by wick or by close, see InpNakedTouch)
   for(int k = 0; k < np; k++)
     {
      g_p[k].touchBar = -1; g_p[k].naked = false; g_p[k].prov = false;
      if(g_p[k].poc == EMPTY_VALUE || k == g_curIdx || g_effPeriod == VP_ROLL) continue;
      g_p[k].touchBar = FirstTouch(k);
      g_p[k].naked = g_p[k].touchBar < 0;
      g_p[k].prov = InpNakedProv && g_p[k].naked && InpNakedTouch == NT_WICK && cLo[N - 1] <= g_p[k].poc && cHi[N - 1] >= g_p[k].poc;
     }
   //---- 6) composite of the primary profiles (resampled, nothing counted twice)
   if(InpComp && g_effPeriod != VP_ROLL && np > 0)
     {
      int src[];
      ArrayResize(src, 0);
      ENUM_VP_PERIOD ck = InpCompMode == COMP_MONTH ? VP_MONTH : VP_WEEK;
      long wk = PeriodIdKind(ck, cTm[N - 1]);
      // CRX-015: a window composite only sums profiles that lie COMPLETELY inside the window; a primary period coarser than the window cannot be cut apart
      bool compBad = (InpCompMode == COMP_WEEK && g_effPeriod == VP_MONTH);
      if(compBad)
         g_status = "Composite 'Current week' needs a primary period of Week or shorter (monthly profiles cannot be split) - composite not built";
      for(int k = np - 1; k >= 0 && !compBad; k--)
        {
         if(!g_p[k].valid) continue;
         if(InpCompMode == COMP_LASTN) { if(ArraySize(src) >= vCompN) break; }
         else
           {
            bool inA = PeriodIdKind(ck, g_p[k].t0) == wk, inB = PeriodIdKind(ck, g_p[k].t1) == wk;
            if(!inA && !inB) break;                                  // wholly before the window
            if(!(inA && inB)) { g_compCut++; break; }                // straddles the window start: partly outside -> left out
           }
         // CRX-010: real and tick volume are different units - only profiles of the SAME type as the newest one are combined
         if(ArraySize(src) > 0 && g_p[k].vtype != g_p[src[0]].vtype) { g_compMixEx++; continue; }
         int m = ArraySize(src); ArrayResize(src, m + 1); src[m] = k;
        }
      if(g_compCut > 0 && g_status == "") g_status = "Composite: the first profile of the window starts before it and was left out (partly outside the window)";
      if(ArraySize(src) > 0) { g_compIdx = MakeComposite(src, ArraySize(src), vCompRows); g_compN = ArraySize(src); }
      if(g_compMixEx > 0 && g_status == "") g_status = "Composite: " + IntegerToString(g_compMixEx) + " profile(s) excluded (different volume type: real vs tick)";
     }
   if(g_realEmpty > 0 && g_status == "") g_status = "Real volume requested but the broker supplies none: " + IntegerToString(g_realEmpty) + " profile(s) empty/invalid";
   //---- 7) anchored profile (exact grid, intrabars)
   if(ancA >= 0)
     {
      double est = 0;
      g_ancIdx = BuildOne(ancA, last, 2, 0, g_ancState == 6 ? 200 : 0, -2, est, vAncRows, false);
     }
   //---- 8) session slots: the latest two sessions of every enabled slot
   for(int s = 0; s < 4; s++)
     {
      if(spn[s] == 0) continue;
      double prevRange = 0;
      for(int q = 0; q < spn[s]; q++)
        {
         int a = sp0[s][q], b = sp1[s][q];
         long pid = CandIdS(a, s + 1);
         double est = prevRange;
         if(est <= 0) { double h0 = cHi[a], l0 = cLo[a]; for(int i = a; i <= b; i++) { if(cHi[i] > h0) h0 = cHi[i]; if(cLo[i] < l0) l0 = cLo[i]; } est = RangeEstimate(a, h0 - l0); }
         int idx = BuildOne(a, b, 3, s, s + 1, pid, est, 0, false, spb[s][q], g_slotS[s], g_slotE[s]);
         prevRange = g_p[idx].hi - g_p[idx].lo;
         if(q == spn[s] - 1) g_msA[s] = idx; else g_msB[s] = idx;
        }
      long formingId = CandIdS(N - 1, s + 1);
      long lastId = CandIdS(sp1[s][spn[s] - 1], s + 1);
      g_msRun[s] = (formingId >= 0 && formingId == lastId);
     }
   g_tot = ArraySize(g_p);
  }

//+------------------------------------------------------------------+
//| Text objects, watermark, panel                                   |
//+------------------------------------------------------------------+
void Label(const string name, const int corner, const int anchor, const int x, const int y,
           const string text, const color clr, const int size, const string font = "Arial")
  {
   if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, corner);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, size);
   ObjectSetString(0, name, OBJPROP_FONT, font);
   ObjectSetString(0, name, OBJPROP_TEXT, text == "" ? " " : text);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 10);
  }

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);
   uint tw = 0, th = 0;
   TextGetSize("crypex", tw, th);
   int wCrypex = (tw > 0) ? (int)tw + 4 : (int)(InpWmSize * 7.0);
   Label(PFX + "WM1", CORNER_LEFT_LOWER, ANCHOR_LEFT_LOWER, InpWmGap, InpWmBottom, "crypex", BRAND_GREEN, InpWmSize, "Arial Black");
   Label(PFX + "WM2", CORNER_LEFT_LOWER, ANCHOR_LEFT_LOWER, InpWmGap + wCrypex, InpWmBottom, ".org", orgCol, InpWmSize, "Arial Black");
  }

string g_rowKey[40];
string g_rowVal[40];
color  g_rowClr[40];
int    g_rows = 0;

void Row(const string k, const string v, const color c)
  {
   if(g_rows >= 40) return;
   g_rowKey[g_rows] = k;
   g_rowVal[g_rows] = v;
   g_rowClr[g_rows] = c;
   g_rows++;
  }

void PaintPanel()
  {
   int W = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), H = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   if(W <= 0 || H <= 0 || g_rows <= 0) return;
   int fs = vPanelFont, pad = 8;
   TextSetFont("Arial", -fs * 10);
   uint tw = 0, th = 0;
   int kw = 0, vw = 0, rh = 0;
   for(int i = 0; i < g_rows; i++)
     {
      TextGetSize(g_rowKey[i], tw, th); if((int)tw > kw) kw = (int)tw; if((int)th > rh) rh = (int)th;
      TextGetSize(g_rowVal[i], tw, th); if((int)tw > vw) vw = (int)tw;
     }
   if(kw <= 0 || rh <= 0) { kw = fs * 14; vw = fs * 24; rh = fs * 2; }
   int keyW = (int)(kw * 1.12) + 16;
   int lineH = MathMax(fs * 2 + 2, rh + 4);
   int pw = pad + keyW + (int)(vw * 1.12) + 18;
   int ph = g_rows * lineH + 10;
   bool right = (InpPanelPos == POS_TOP_RIGHT || InpPanelPos == POS_BOTTOM_RIGHT);
   bool bottom = (InpPanelPos == POS_BOTTOM_LEFT || InpPanelPos == POS_BOTTOM_RIGHT);
   int botMargin = (bottom && !right && InpShowWm) ? 8 + InpWmBottom + (int)(InpWmSize * 2.3) + 6 : 8;
   int x0 = right ? W - 8 - pw : 8;
   int y0 = bottom ? H - botMargin - ph : 8;
   if(x0 < 0) x0 = 0;
   if(y0 < 0) y0 = 0;

   string bgName = PFX + "PBG";
   if(ObjectFind(0, bgName) < 0) ObjectCreate(0, bgName, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, bgName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, bgName, OBJPROP_XDISTANCE, x0);
   ObjectSetInteger(0, bgName, OBJPROP_YDISTANCE, y0);
   ObjectSetInteger(0, bgName, OBJPROP_XSIZE, pw);
   ObjectSetInteger(0, bgName, OBJPROP_YSIZE, ph);
   ObjectSetInteger(0, bgName, OBJPROP_BGCOLOR, InpPanelBg);
   ObjectSetInteger(0, bgName, OBJPROP_COLOR, C'70,76,86');
   ObjectSetInteger(0, bgName, OBJPROP_BORDER_TYPE, 0);
   ObjectSetInteger(0, bgName, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, bgName, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, bgName, OBJPROP_ZORDER, 5);
   for(int i = 0; i < 40; i++)
     {
      string nk = PFX + "PK" + IntegerToString(i);
      string nv = PFX + "PV" + IntegerToString(i);
      if(i >= g_rows) { ObjectDelete(0, nk); ObjectDelete(0, nv); continue; }
      int y = y0 + 6 + i * lineH;
      Label(nk, CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER, x0 + pad, y, g_rowKey[i], i == 0 ? clrWhite : C'150,156,166', fs);
      Label(nv, CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER, x0 + pad + keyW, y, g_rowVal[i], g_rowClr[i], fs, i == 0 ? "Arial Bold" : "Arial");
     }
  }

//+------------------------------------------------------------------+
//| Drawing                                                          |
//+------------------------------------------------------------------+
void Rect(const string name, const datetime t1, const double p1, const datetime t2, const double p2, const color c)
  {
   if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_RECTANGLE, 0, t1, p1, t2, p2);
   ObjectSetInteger(0, name, OBJPROP_TIME, 0, t1);
   ObjectSetDouble(0, name, OBJPROP_PRICE, 0, p1);
   ObjectSetInteger(0, name, OBJPROP_TIME, 1, t2);
   ObjectSetDouble(0, name, OBJPROP_PRICE, 1, p2);
   ObjectSetInteger(0, name, OBJPROP_COLOR, c);
   ObjectSetInteger(0, name, OBJPROP_FILL, true);
   ObjectSetInteger(0, name, OBJPROP_BACK, true);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

void HLine(const string name, const datetime t1, const datetime t2, const double p, const color c, const int w, const ENUM_LINE_STYLE st)
  {
   if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_TREND, 0, t1, p, t2, p);
   ObjectSetInteger(0, name, OBJPROP_TIME, 0, t1);
   ObjectSetDouble(0, name, OBJPROP_PRICE, 0, p);
   ObjectSetInteger(0, name, OBJPROP_TIME, 1, t2);
   ObjectSetDouble(0, name, OBJPROP_PRICE, 1, p);
   ObjectSetInteger(0, name, OBJPROP_COLOR, c);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, w);
   ObjectSetInteger(0, name, OBJPROP_STYLE, st);
   ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false);
   ObjectSetInteger(0, name, OBJPROP_BACK, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

void Txt(const string name, const datetime t, const double p, const string text, const color c)
  {
   if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_TEXT, 0, t, p);
   ObjectSetInteger(0, name, OBJPROP_TIME, 0, t);
   ObjectSetDouble(0, name, OBJPROP_PRICE, 0, p);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_COLOR, c);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, vLabelSize);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_LEFT);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

void DrawProfile(const int k, const bool isCur, const bool isLatestDone, const int wBars, int &nodeLeft)
  {
   string tag = PFX + "PR" + IntegerToString(k) + "_";
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   if(n <= 0 || g_p[k].poc == EMPTY_VALUE) return;
   datetime tL = g_p[k].t0;
   datetime tR = (isCur || (isLatestDone && InpExtendPrev)) ? cTm[N - 1] + (datetime)((long)g_chartSec * vLineExt) : g_p[k].t1 + g_chartSec;
   datetime tEnd = g_p[k].t1 + g_chartSec;
   long bg = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   color bgc = (color)bg;
   if(InpShadeVA)
      Rect(tag + "VA", tL, g_p[k].vah, tR, g_p[k].val, Blend(InpVaLineColor, bgc, 93));
   if(InpShowHist)
     {
      double mx = 0;
      for(int i = 0; i < n; i++) mx = MathMax(mx, g_up[s0 + i] + g_dn[s0 + i]);
      bool leftDir = (InpDir == DIR_LEFT_RIGHT);
      datetime anchor = leftDir ? tL : tEnd;
      if(mx > 0)
         for(int i = 0; i < n; i++)
           {
            double tot = g_up[s0 + i] + g_dn[s0 + i];
            int len = (int)MathRound(tot / mx * wBars);
            if(len < 1) continue;
            bool inVA = (i >= g_p[k].vaLo && i <= g_p[k].vaHi);
            double yT = g_p[k].off + (g_p[k].base + i + 1) * g_p[k].rs;
            double yB = g_p[k].off + (g_p[k].base + i) * g_p[k].rs;
            string nm = tag + "H" + IntegerToString(i);
            if(InpSplitUD)
              {
               int upLen = (int)MathRound(g_up[s0 + i] / mx * wBars);
               if(upLen > len) upLen = len;
               int dnLen = len - upLen;
               color cU = (i == g_p[k].pocI) ? InpPocRowColor : Blend(InpUpColor, bgc, inVA ? vInTransp : vOutTransp);
               color cD = (i == g_p[k].pocI) ? InpPocRowColor : Blend(InpDnColor, bgc, inVA ? vInTransp : vOutTransp);
               long ps = g_chartSec;
               if(leftDir)
                 {
                  if(upLen > 0) Rect(nm + "u", anchor, yT, anchor + (datetime)(ps * upLen), yB, cU);
                  if(dnLen > 0) Rect(nm + "d", anchor + (datetime)(ps * upLen), yT, anchor + (datetime)(ps * len), yB, cD);
                 }
               else
                 {
                  if(upLen > 0) Rect(nm + "u", anchor - (datetime)(ps * upLen), yT, anchor, yB, cU);
                  if(dnLen > 0) Rect(nm + "d", anchor - (datetime)(ps * len), yT, anchor - (datetime)(ps * upLen), yB, cD);
                 }
              }
            else
              {
               color cS = (i == g_p[k].pocI) ? InpPocRowColor : Blend(inVA ? InpVaColor : InpOutColor, bgc, inVA ? vInTransp : vOutTransp);
               long ps = g_chartSec;
               if(leftDir) Rect(nm + "s", anchor, yT, anchor + (datetime)(ps * len), yB, cS);
               else        Rect(nm + "s", anchor - (datetime)(ps * len), yT, anchor, yB, cS);
              }
           }
     }
   string pre = isCur ? "" : "p";
   if(InpShowPoc) HLine(tag + "POC", tL, tR, g_p[k].poc, InpPocColor, vPocWidth, STYLE_SOLID);
   if(InpShowVA)
     {
      HLine(tag + "VAH", tL, tR, g_p[k].vah, InpVaLineColor, vVaLineWidth, STYLE_SOLID);
      HLine(tag + "VAL", tL, tR, g_p[k].val, InpVaLineColor, vVaLineWidth, STYLE_SOLID);
     }
   if(InpShowLabels && (isCur || isLatestDone))
     {
      if(InpShowPoc) Txt(tag + "TPOC", tR, g_p[k].poc, " " + pre + "POC " + Px(g_p[k].poc), InpPocColor);
      if(InpShowVA)
        {
         Txt(tag + "TVAH", tR, g_p[k].vah, " " + pre + "VAH " + Px(g_p[k].vah), InpVaLineColor);
         Txt(tag + "TVAL", tR, g_p[k].val, " " + pre + "VAL " + Px(g_p[k].val), InpVaLineColor);
        }
     }
   if(InpNodes && nodeLeft > 0)
     {
      datetime nx1 = g_p[k].t0 + (datetime)((long)g_chartSec * MathMax(wBars, 8));
      long bgn = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
      if(InpNodeMode == NODE_ADV)
        {
         SNode hv[], lv[];
         double sm[];
         AdvNodes(k, hv, lv, sm);
         for(int q = 0; q < ArraySize(hv); q++)
           {
            double yy = g_p[k].off + (g_p[k].base + hv[q].idx + 0.5) * g_p[k].rs;
            if(InpNodeZones)
               Rect(tag + "NHZ" + IntegerToString(q), g_p[k].t0, g_p[k].off + (g_p[k].base + hv[q].zh + 1) * g_p[k].rs, nx1, g_p[k].off + (g_p[k].base + hv[q].zl) * g_p[k].rs, Blend(InpHvnColor, (color)bgn, 86));
            HLine(tag + "NH" + IntegerToString(q), g_p[k].t0, nx1, yy, InpHvnColor, 1, STYLE_DASH);
            Txt(tag + "NHT" + IntegerToString(q), nx1, yy, " HVN", InpHvnColor);
           }
         for(int q = 0; q < ArraySize(lv); q++)
           {
            double yy = g_p[k].off + (g_p[k].base + lv[q].idx + 0.5) * g_p[k].rs;
            if(InpNodeZones)
               Rect(tag + "NLZ" + IntegerToString(q), g_p[k].t0, g_p[k].off + (g_p[k].base + lv[q].zh + 1) * g_p[k].rs, nx1, g_p[k].off + (g_p[k].base + lv[q].zl) * g_p[k].rs, Blend(InpLvnColor, (color)bgn, 88));
            HLine(tag + "NL" + IntegerToString(q), g_p[k].t0, nx1, yy, InpLvnColor, 1, STYLE_DOT);
            Txt(tag + "NLT" + IntegerToString(q), nx1, yy, " LVN", InpLvnColor);
           }
        }
      else
        {
         int hv[], lv[];
         FindNodes(k, hv, lv);
         for(int q = 0; q < ArraySize(hv); q++)
           {
            double yy = g_p[k].off + (g_p[k].base + hv[q] + 0.5) * g_p[k].rs;
            HLine(tag + "NH" + IntegerToString(q), g_p[k].t0, nx1, yy, InpHvnColor, 1, STYLE_DASH);
            Txt(tag + "NHT" + IntegerToString(q), nx1, yy, " HVN", InpHvnColor);
           }
         for(int q = 0; q < ArraySize(lv); q++)
           {
            double yy = g_p[k].off + (g_p[k].base + lv[q] + 0.5) * g_p[k].rs;
            HLine(tag + "NL" + IntegerToString(q), g_p[k].t0, nx1, yy, InpLvnColor, 1, STYLE_DOT);
            Txt(tag + "NLT" + IntegerToString(q), nx1, yy, " LVN", InpLvnColor);
           }
        }
      nodeLeft--;
     }
  }

// composite / anchored / session profile: POC, VAH, VAL lines (+ optional single-colour histogram)
void DrawSpecial(const int k, const string tag, const color c, const string label, const bool hist, const bool fromRight, const int wBars, const bool shadeVa)
  {
   if(k < 0 || !g_p[k].valid || g_p[k].poc == EMPTY_VALUE) return;
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   datetime tL = g_p[k].t0;
   datetime tR = cTm[N - 1] + (datetime)((long)g_chartSec * vLineExt);
   long bg = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   if(shadeVa) Rect(tag + "VA", tL, g_p[k].vah, tR, g_p[k].val, Blend(c, (color)bg, 92));
   if(hist)
     {
      double mx = 0;
      for(int i = 0; i < n; i++) mx = MathMax(mx, g_up[s0 + i] + g_dn[s0 + i]);
      datetime anchor = fromRight ? tR : tL;
      if(mx > 0)
         for(int i = 0; i < n; i++)
           {
            int len = (int)MathRound((g_up[s0 + i] + g_dn[s0 + i]) / mx * wBars);
            if(len < 1) continue;
            bool inVA = (i >= g_p[k].vaLo && i <= g_p[k].vaHi);
            double yT = g_p[k].off + (g_p[k].base + i + 1) * g_p[k].rs, yB = g_p[k].off + (g_p[k].base + i) * g_p[k].rs;
            color cc = (i == g_p[k].pocI) ? c : Blend(c, (color)bg, inVA ? 45 : 75);
            long ps = g_chartSec;
            if(fromRight) Rect(tag + "H" + IntegerToString(i), anchor - (datetime)(ps * len), yT, anchor, yB, cc);
            else          Rect(tag + "H" + IntegerToString(i), anchor, yT, anchor + (datetime)(ps * len), yB, cc);
           }
     }
   HLine(tag + "POC", tL, tR, g_p[k].poc, c, 2, STYLE_SOLID);
   HLine(tag + "VAH", tL, tR, g_p[k].vah, c, 1, STYLE_DASH);
   HLine(tag + "VAL", tL, tR, g_p[k].val, c, 1, STYLE_DASH);
   if(InpShowLabels)
     {
      Txt(tag + "TPOC", tR, g_p[k].poc, " " + label + " POC " + Px(g_p[k].poc), c);
      Txt(tag + "TVAH", tR, g_p[k].vah, " " + label + " VAH " + Px(g_p[k].vah), c);
      Txt(tag + "TVAL", tR, g_p[k].val, " " + label + " VAL " + Px(g_p[k].val), c);
     }
  }

void DrawIB(const int k)
  {
   if(k < 0 || g_p[k].ibH == EMPTY_VALUE || g_p[k].ibL == EMPTY_VALUE) return;
   string tag = PFX + "PRIB_";
   datetime tL = g_p[k].t0, tR = cTm[N - 1] + (datetime)((long)g_chartSec * vLineExt);
   double h = g_p[k].ibH, l = g_p[k].ibL, r = h - l;
   HLine(tag + "H", tL, tR, h, InpIBColor, 2, STYLE_SOLID);
   HLine(tag + "L", tL, tR, l, InpIBColor, 2, STYLE_SOLID);
   Txt(tag + "TH", tR, h, " IBH " + Px(h), InpIBColor);
   Txt(tag + "TL", tR, l, " IBL " + Px(l), InpIBColor);
   if(InpIBExt && r > 0)
      for(int m = 1; m <= 2; m++)
        {
         HLine(tag + "EH" + IntegerToString(m), tL, tR, h + m * r, InpIBColor, 1, STYLE_DOT);
         HLine(tag + "EL" + IntegerToString(m), tL, tR, l - m * r, InpIBColor, 1, STYLE_DOT);
         Txt(tag + "TEH" + IntegerToString(m), tR, h + m * r, " IB+" + IntegerToString(m) + "x", InpIBColor);
         Txt(tag + "TEL" + IntegerToString(m), tR, l - m * r, " IB-" + IntegerToString(m) + "x", InpIBColor);
        }
  }

void AddLv(double &pr[], string &nm[], string &sk[], int &cnt, const double price, const string name, const string key)
  {
   if(price == EMPTY_VALUE) return;
   ArrayResize(pr, cnt + 1); ArrayResize(nm, cnt + 1); ArrayResize(sk, cnt + 1);
   pr[cnt] = price; nm[cnt] = name; sk[cnt] = key; cnt++;
  }

double AtrN(const int period)
  {
   double s = 0;
   int cnt = 0;
   for(int i = N - 2; i >= 1 && cnt < period; i--, cnt++)
      s += MathMax(cHi[i] - cLo[i], MathMax(MathAbs(cHi[i] - cCl[i - 1]), MathAbs(cLo[i] - cCl[i - 1])));
   return cnt > 0 ? s / cnt : 0.0;
  }

int g_confZones = 0;

// key levels that overlap: previous VA, composite, anchored, IB, active naked POCs and HVN centres
void PaintConfluence(const int done, const int cur)
  {
   double pr[];
   string nm[], sk[];
   ArrayResize(pr, 0); ArrayResize(nm, 0); ArrayResize(sk, 0);
   int cnt = 0;
   if(done >= 0) { AddLv(pr, nm, sk, cnt, g_p[done].poc, "pPOC", "P" + IntegerToString(done)); AddLv(pr, nm, sk, cnt, g_p[done].vah, "pVAH", "P" + IntegerToString(done)); AddLv(pr, nm, sk, cnt, g_p[done].val, "pVAL", "P" + IntegerToString(done)); }
   if(g_compIdx >= 0) { AddLv(pr, nm, sk, cnt, g_p[g_compIdx].poc, "cPOC", "C"); AddLv(pr, nm, sk, cnt, g_p[g_compIdx].vah, "cVAH", "C"); AddLv(pr, nm, sk, cnt, g_p[g_compIdx].val, "cVAL", "C"); }
   if(g_ancIdx >= 0) { AddLv(pr, nm, sk, cnt, g_p[g_ancIdx].poc, "aPOC", "A"); }
   int refIB = cur >= 0 ? cur : done;
   if(InpIB && refIB >= 0) { AddLv(pr, nm, sk, cnt, g_p[refIB].ibH, "IBH", "P" + IntegerToString(refIB)); AddLv(pr, nm, sk, cnt, g_p[refIB].ibL, "IBL", "P" + IntegerToString(refIB)); }
   if(InpNaked && done >= 0)
      for(int k = done; k >= 0; k--)
         if(g_p[k].naked && (InpNakedMaxAge <= 0 || (N - 1 - g_p[k].i1) <= InpNakedMaxAge)) AddLv(pr, nm, sk, cnt, g_p[k].poc, "nPOC", "P" + IntegerToString(k));
   int hvSrc[2];
   hvSrc[0] = cur; hvSrc[1] = done;
   for(int z = 0; z < 2; z++)
     {
      int k = hvSrc[z];
      if(k < 0 || (z == 1 && k == hvSrc[0])) continue;
      SNode hv[], lv[];
      double sm[];
      AdvNodes(k, hv, lv, sm);
      for(int q = 0; q < ArraySize(hv); q++) AddLv(pr, nm, sk, cnt, g_p[k].off + (g_p[k].base + hv[q].idx + 0.5) * g_p[k].rs, "HVN", "P" + IntegerToString(k));
     }
   double tol = vConfTol * AtrN(14);
   double zlo[], zhi[];
   int zn[];
   string zs[];
   int nz = tol > 0 ? ClusterLevels(pr, nm, sk, cnt, tol, vConfMin, zlo, zhi, zn, zs) : 0;
   g_confZones = nz;
   if(nz == 0) return;
   double px = cCl[N - 2];
   int order[];
   ArrayResize(order, nz);
   for(int i = 0; i < nz; i++) order[i] = i;
   for(int a = 1; a < nz; a++)
     {
      int v = order[a], b = a - 1;
      while(b >= 0 && MathAbs((zlo[order[b]] + zhi[order[b]]) / 2 - px) > MathAbs((zlo[v] + zhi[v]) / 2 - px)) { order[b + 1] = order[b]; b--; }
      order[b + 1] = v;
     }
   long bg = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   datetime tL = cTm[MathMax(0, N - 80)], tR = cTm[N - 1] + (datetime)((long)g_chartSec * vLineExt);
   for(int q = 0; q < nz && q < vConfMax; q++)
     {
      int z = order[q];
      double pad = tol * 0.25;
      Rect(PFX + "PRCF" + IntegerToString(q), tL, zhi[z] + pad, tR, zlo[z] - pad, Blend(InpConfColor, (color)bg, 78));
      Txt(PFX + "PRCFT" + IntegerToString(q), tR, (zlo[z] + zhi[z]) / 2, " Confluence x" + IntegerToString(zn[z]) + " " + zs[z], InpConfColor);
     }
  }

void PaintAll()
  {
   ObjectsDeleteAll(0, PFX + "PR");
   ObjectsDeleteAll(0, PFX + "NK");
   g_confZones = 0;
   if(g_tot == 0) return;
   int cur = g_curIdx, done = g_doneIdx;
   int wBars = 10;
   if(g_np > 0)
     {
      int refA = cur >= 0 ? cur : done;
      int span = g_p[refA].i1 - g_p[refA].i0 + 1;
      if(cur >= 0 && done >= 0) span = MathMax(span, g_p[done].i1 - g_p[done].i0 + 1);
      if(g_effPeriod == VP_ROLL) span = vRollN;
      wBars = MathMax(3, (int)MathRound(span * vWidthPct / 100.0));
      int nodeLeft = vNodeProfs;
      if(cur >= 0 && g_p[cur].valid) DrawProfile(cur, true, false, wBars, nodeLeft);
      if(g_effPeriod != VP_ROLL && done >= 0)
        {
         for(int k = done; k >= MathMax(0, done - vKeepPrev + 1) && vKeepPrev > 0; k--)
            if(g_p[k].valid) DrawProfile(k, false, k == done, wBars, nodeLeft);
         if(InpNaked)
           {
            int shown = 0, consumed = 0;
            for(int k = done; k >= 0 && (shown < vNakedMax || consumed < vNakedConsumed); k--)
              {
               if(!g_p[k].valid || g_p[k].poc == EMPTY_VALUE) continue;
               int age = N - 1 - g_p[k].i1;
               datetime tR = cTm[N - 1] + (datetime)((long)g_chartSec * vLineExt);
               string nm = PFX + "NK" + IntegerToString(k);
               string dstr = TimeToString(g_p[k].t0, TIME_DATE);
               if(g_p[k].naked)
                 {
                  if(shown >= vNakedMax || (InpNakedMaxAge > 0 && age > InpNakedMaxAge)) continue;
                  double dist = (g_p[k].poc - cCl[N - 2]) / cCl[N - 2] * 100.0;
                  HLine(nm, g_p[k].t1, tR, g_p[k].poc, InpNakedColor, 1, STYLE_DASH);
                  Txt(nm + "T", tR, g_p[k].poc, " nPOC " + dstr + " " + Px(g_p[k].poc) + " · " + IntegerToString(age) + " bars · " + DoubleToString(dist, 2) + "%" + (g_p[k].prov ? " · provisional touch" : ""), InpNakedColor);
                  shown++;
                 }
               else if(g_p[k].touchBar >= 0 && consumed < vNakedConsumed)
                 {
                  long bgc = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
                  color fc = Blend(InpNakedColor, (color)bgc, 65);
                  HLine(nm, g_p[k].t1, cTm[g_p[k].touchBar], g_p[k].poc, fc, 1, STYLE_DOT);
                  Txt(nm + "T", cTm[g_p[k].touchBar], g_p[k].poc, " touched " + dstr + " " + Px(g_p[k].poc), fc);
                  consumed++;
                 }
              }
           }
        }
      int refIB = cur >= 0 ? cur : done;
      if(InpIB && refIB >= 0) DrawIB(refIB);
     }
   if(g_compIdx >= 0) DrawSpecial(g_compIdx, PFX + "PRCP_", InpCompColor, "Comp", InpCompHist, true, MathMax(8, wBars), false);
   if(g_ancIdx >= 0)  DrawSpecial(g_ancIdx, PFX + "PRAN_", InpAncColor, "Anch", InpAncHist, false, MathMax(8, wBars), false);
   for(int s = 0; s < 4; s++)
     {
      if(g_msB[s] >= 0) DrawSpecial(g_msB[s], PFX + "PRMB" + IntegerToString(s) + "_", Blend(SlotColor(s), (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND), 40), "p" + SlotName(s), false, false, 8, false);
      if(g_msA[s] >= 0) DrawSpecial(g_msA[s], PFX + "PRMA" + IntegerToString(s) + "_", SlotColor(s), SlotName(s), InpMsHist, false, MathMax(8, wBars), false);
     }
   if(InpConf) PaintConfluence(done, cur);
  }

//+------------------------------------------------------------------+
//| Panel content and alerts                                         |
//+------------------------------------------------------------------+
void Notify(const string msg)
  {
   if(InpAlertPopup) Alert(msg);
   if(InpAlertPush)  { if(!SendNotification(msg)) Print("Crypex VP: push notification failed, error ", GetLastError()); }
   if(InpAlertMail)  { if(!SendMail("Crypex Volume Profile", msg)) Print("Crypex VP: e-mail failed, error ", GetLastError()); }
  }

// CRX-018: levels of the latest FINISHED profile are only alertable when that profile is valid and the data basis is usable
bool AlertLevelsOk()
  {
   if(g_effPeriod == VP_ROLL || g_doneIdx < 0 || g_strictNA) return false;
   if(!g_p[g_doneIdx].valid) return false;
   if(InpAlertBlockPoor && QualityPoor(g_doneIdx)) return false;          // CRX-029: mathematically valid is not the same as data-quality acceptable
   return true;
  }

string AlertTag(const int pv)
  {
   return QualityPoor(pv) ? " [previous profile data INCOMPLETE: " + QualityText(pv) + "]" : "";
  }

// confirmed-bar alerts against the latest FINISHED profile; the very first evaluation only arms the detector (no historical alert)
void CheckAlerts()
  {
   if(g_effPeriod == VP_ROLL || N < 5) return;
   if(InpAlertHealth)
     {
      int hs = (g_strictNA ? 1 : 0) | (g_tzState == 2 ? 2 : 0) | ((g_doneIdx >= 0 && QualityPoor(g_doneIdx)) ? 4 : 0);
      if(hs != g_healthSeen && g_alertInit)
        {
         if((hs & 1) != 0 && (g_healthSeen & 1) == 0) Notify(_Symbol + ": Crypex VP - Strict intrabars UNAVAILABLE, profiles are not built");
         if((hs & 2) != 0 && (g_healthSeen & 2) == 0) Notify(_Symbol + ": Crypex VP - server time model does NOT match the terminal clock; session/day boundaries unreliable");
         if((hs & 4) != 0 && (g_healthSeen & 4) == 0) Notify(_Symbol + ": Crypex VP - previous profile built from INCOMPLETE data: " + QualityText(g_doneIdx));
        }
      g_healthSeen = hs;
     }
   if(cTm[N - 2] == g_lastAlertBar) return;
   g_lastAlertBar = cTm[N - 2];
   if(!g_alertInit) { g_alertInit = true; return; }
   if(PeriodId(cTm[N - 2]) != PeriodId(cTm[N - 3]))                          // do not compare closes across a period change
     {
      if(InpAlertNewPeriod && AlertLevelsOk())
         Notify(_Symbol + " " + EnumToString(_Period) + ": new period - previous POC " + Px(g_p[g_doneIdx].poc) + " VAH " + Px(g_p[g_doneIdx].vah) + " VAL " + Px(g_p[g_doneIdx].val) + AlertTag(g_doneIdx));
      return;
     }
   if(InpAlertMode != AL_CLOSE || !AlertLevelsOk()) return;
   int pv = g_doneIdx;
   double c1 = cCl[N - 2], c0 = cCl[N - 3];
   double lv[3];
   string nm[3] = {"POC", "VAH", "VAL"};
   lv[0] = g_p[pv].poc; lv[1] = g_p[pv].vah; lv[2] = g_p[pv].val;
   for(int q = 0; q < 3; q++)
     {
      if(lv[q] == EMPTY_VALUE) continue;
      if(c0 <= lv[q] && c1 > lv[q]) Notify(_Symbol + " " + EnumToString(_Period) + ": candle CLOSED above previous " + nm[q] + " " + Px(lv[q]) + AlertTag(pv));
      if(c0 >= lv[q] && c1 < lv[q]) Notify(_Symbol + " " + EnumToString(_Period) + ": candle CLOSED below previous " + nm[q] + " " + Px(lv[q]) + AlertTag(pv));
     }
  }

// live-cross alerts: evaluated on every tick, at most once per level/direction per bar (a live cross can repeat while the candle is open)
void CheckLive(const double px)
  {
   if(InpAlertMode != AL_LIVE || !g_alertInit || !AlertLevelsOk() || N < 2 || px <= 0) return;
   if(g_livePx <= 0) { g_livePx = px; return; }
   int pv = g_doneIdx;
   double lv[3];
   string nm[3] = {"POC", "VAH", "VAL"};
   lv[0] = g_p[pv].poc; lv[1] = g_p[pv].vah; lv[2] = g_p[pv].val;
   for(int q = 0; q < 3; q++)
     {
      if(lv[q] == EMPTY_VALUE) continue;
      if(g_livePx <= lv[q] && px > lv[q] && g_liveBar[q * 2] != cTm[N - 1]) { g_liveBar[q * 2] = cTm[N - 1]; Notify(_Symbol + " " + EnumToString(_Period) + ": price crossed ABOVE previous " + nm[q] + " " + Px(lv[q]) + " (live, candle not closed)" + AlertTag(pv)); }
      if(g_livePx >= lv[q] && px < lv[q] && g_liveBar[q * 2 + 1] != cTm[N - 1]) { g_liveBar[q * 2 + 1] = cTm[N - 1]; Notify(_Symbol + " " + EnumToString(_Period) + ": price crossed BELOW previous " + nm[q] + " " + Px(lv[q]) + " (live, candle not closed)" + AlertTag(pv)); }
     }
   g_livePx = px;
  }

string VolKindText()
  {
   if(g_nReal > 0 && g_nTick == 0) return "Real volume";
   if(g_nReal == 0) return "Tick volume (approx.)";
   return "Mixed (real / tick per profile)";
  }

void BuildPanel()
  {
   g_rows = 0;
   color gry = C'150,156,166', grn = C'38,166,91', red = C'239,83,80', org = C'255,152,0';
   string vk = VolKindText();
   Row("Crypex VP", _Symbol, clrWhite);
   if(InpShowTickRow) Row("Tick size (eff.)", DoubleToString(g_tick, 8) + " · " + g_tickSrc + " (raw " + DoubleToString(g_tickRaw, 8) + ")", clrWhite);
   if(UsesTzConversion())
     {
      string dr = "no DST";
      if(InpServerDst == DST_US) dr = "US DST";
      else if(InpServerDst == DST_EU) dr = "EU DST";
      else if(InpServerDst == DST_MANUAL) dr = "manual schedule (" + IntegerToString(g_schN) + " entries)";
      string sgS = vServerOffset >= 0 ? "+" : "";
      string tzs = "server = UTC" + sgS + DoubleToString(vServerOffset, 1) + " winter · " + dr;
      if(g_tzState == 1) Row("Time basis", tzs + " · matches terminal clock", grn);
      else if(g_tzState == 2)
        {
         string sgO = g_tzObs >= 0 ? "+" : "", sgM = g_tzMdl >= 0 ? "+" : "";
         Row("Time basis", tzs + " · MISMATCH: terminal reports UTC" + sgO + DoubleToString(g_tzObs, 2) + ", model UTC" + sgM + DoubleToString(g_tzMdl, 2) + " - session/day boundaries are NOT reliable", red);
        }
      else Row("Time basis", tzs + " · ASSUMED (not verifiable now) - boundaries are only as right as this setting", org);
     }
   Row("Volume type", vk, vk == "Real volume" ? grn : org);
   string src = (g_effPeriod == VP_ROLL || !g_useLtf) ? "Chart bars" : StringSubstr(EnumToString(g_ltf), 7) + " intrabars";
   if(g_strictNA) src = "UNAVAILABLE (Strict, no lower timeframe)";
   else if(g_effPeriod == VP_ROLL && InpVolSrc == SRC_STRICT) src = "Chart bars (Rolling mode has no intrabars)";
   long cv = g_ltfBars + g_fbBars + g_skipBars;
   if(g_effPeriod != VP_ROLL && g_useLtf && cv > 0)
     {
      src += " · " + IntegerToString((int)(g_ltfBars * 100 / cv)) + "% intrabar";
      if(g_cvExp > 0) src += " · time cov " + DoubleToString(100.0 * g_cvGot / g_cvExp, 1) + "%" + (g_cvVolChart > 0 ? " · vol cov " + DoubleToString(100.0 * g_cvVolIn / g_cvVolChart, 1) + "%" : "");
      if(g_cvSparse > 0)  src += " · " + IntegerToString((int)g_cvSparse) + " sparse (no-trade minutes)";
      if(g_cvPartial > 0) src += " · " + IntegerToString((int)g_cvPartial) + " PARTIAL (Strict, volume mismatch)";
      if(g_cvTypeMis > 0) src += " · " + IntegerToString((int)g_cvTypeMis) + " volume-type mismatch";
      if(g_fbBars > 0)   src += " · " + IntegerToString((int)(g_fbBars * 100 / cv)) + "% chart-bar";
      if(g_skipBars > 0) src += " · " + IntegerToString((int)(g_skipBars * 100 / cv)) + "% missing";
     }
   Row("Source", src, clrWhite);
   string per = (InpPeriod == VP_AUTO ? "Auto → " : "") + (g_effPeriod == VP_DAY ? "Day" : g_effPeriod == VP_WEEK ? "Week" : g_effPeriod == VP_MONTH ? "Month" : g_effPeriod == VP_SESSION ? "Session " + InpSessStart + "-" + InpSessEnd : "Last " + IntegerToString(vRollN) + " bars");
   Row("Period", per, clrWhite);
   double px = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   if(g_curIdx >= 0)
     {
      int c = g_curIdx;
      Row("Rows", IntegerToString(g_p[c].rowN) + " × " + DoubleToString(g_p[c].rs, _Digits), clrWhite);
      if(g_effPeriod != VP_ROLL) Row("Data · current", QualityText(c), QualityPoor(c) ? org : grn);
      Row("POC", Px(g_p[c].poc), InpPocColor);
      Row("VAH / VAL", Px(g_p[c].vah) + " / " + Px(g_p[c].val), InpVaLineColor);
      string where = "—"; color wc = gry;
      if(g_p[c].vah != EMPTY_VALUE)
        {
         where = px > g_p[c].vah ? "Above VA" : (px < g_p[c].val ? "Below VA" : "Inside VA");
         wc = px > g_p[c].vah ? grn : (px < g_p[c].val ? red : gry);
         if(g_p[c].poc > 0) where += " · " + DoubleToString((px - g_p[c].poc) / g_p[c].poc * 100.0, 2) + "% from POC";
        }
      Row("Price", where, wc);
      if(g_p[c].total > 0)
        {
         double upP = g_p[c].upTotal / g_p[c].total * 100.0;
         Row("Est. up/down vol", "Up " + DoubleToString(upP, 0) + "% / Down " + DoubleToString(100 - upP, 0) + "%", upP >= 52 ? grn : (upP <= 48 ? red : gry));
        }
     }
   else if(g_np > 0) Row("Current", "waiting for the first closed bar", org);
   if(g_effPeriod != VP_ROLL && g_doneIdx >= 0)
     {
      int pv = g_doneIdx;
      Row("Prev POC", Px(g_p[pv].poc), InpPocColor);
      Row("Prev VAH / VAL", Px(g_p[pv].vah) + " / " + Px(g_p[pv].val), InpVaLineColor);
      int cnt = 0; double nearest = EMPTY_VALUE;
      for(int k = 0; k <= g_doneIdx; k++)
         if(g_p[k].naked && (InpNakedMaxAge <= 0 || (N - 1 - g_p[k].i1) <= InpNakedMaxAge))
           {
            cnt++;
            if(nearest == EMPTY_VALUE || MathAbs(g_p[k].poc - px) < MathAbs(nearest - px)) nearest = g_p[k].poc;
           }
      Row("Naked POCs", cnt == 0 ? "None" : IntegerToString(cnt) + " · nearest " + Px(nearest), InpNakedColor);
     }
   //---- analytics describing the current profile (or the latest finished one) against the previous profile
   int A = g_curIdx >= 0 ? g_curIdx : g_doneIdx;
   if(g_effPeriod != VP_ROLL && A >= 1 && g_p[A].poc != EMPTY_VALUE && g_p[A - 1].poc != EMPTY_VALUE)
     {
      int Bp = A - 1;
      if(InpMigration)
        {
         double d = g_p[A].poc - g_p[Bp].poc;
         string st = MigrationState(d, g_p[A].rs);
         Row("POC migration", (d >= 0 ? "+" : "") + DoubleToString(d / g_tick, 0) + " ticks (" + (d >= 0 ? "+" : "") + DoubleToString(d / g_p[Bp].poc * 100.0, 2) + "%) · " + st, st == "Stable" ? gry : (d > 0 ? grn : red));
        }
      if(InpVaShift)
        {
         string vs = VaShiftText(g_p[A].val, g_p[A].vah, g_p[Bp].val, g_p[Bp].vah);
         Row("VA vs previous", vs, vs == "Higher value" ? grn : (vs == "Lower value" ? red : clrWhite));
        }
     }
   if(InpMigration && InpShowDev && g_curIdx >= 0 && N >= 4)
     {
      double d1 = BufPoc[g_bufOff + N - 2], d0 = BufPoc[g_bufOff + N - 3];
      if(d1 != EMPTY_VALUE && d0 != EMPTY_VALUE && d1 != 0 && d0 != 0)
         Row("Dev POC last bar", (d1 - d0 >= 0 ? "+" : "") + DoubleToString((d1 - d0) / g_tick, 0) + " ticks", d1 > d0 ? grn : (d1 < d0 ? red : gry));
     }
   if(InpShape && A >= 0) Row("Shape (heuristic)", ShapeOf(A), clrWhite);
   if(InpRvol && g_curIdx >= 0)
     {
      int used;
      double rv = RvolOf(g_curIdx, used);
      Row("Rel. volume", rv < 0 ? "n/a (needs 2+ previous)" : DoubleToString(rv, 2) + "x · avg of " + IntegerToString(used), rv >= 1.5 ? grn : (rv < 0 ? gry : (rv < 0.7 ? org : clrWhite)));
     }
   if(InpIB && A >= 0 && g_p[A].ibH != EMPTY_VALUE)
     {
      double r = g_p[A].ibH - g_p[A].ibL;
      Row("IB high / low", Px(g_p[A].ibH) + " / " + Px(g_p[A].ibL) + (g_p[A].ibEst ? " (approx.: partial candle without intrabars left out)" : ""), InpIBColor);
      string ibs = "Inside IB";
      if(r > 0 && px > g_p[A].ibH) ibs = "Above IB +" + DoubleToString((px - g_p[A].ibH) / r, 2) + "x";
      else if(r > 0 && px < g_p[A].ibL) ibs = "Below IB -" + DoubleToString((g_p[A].ibL - px) / r, 2) + "x";
      Row("IB range", Px(r) + " · " + ibs, clrWhite);
     }
   if(g_effPeriod != VP_ROLL && g_doneIdx >= 0 && g_doneIdx != g_curIdx) Row("Data · previous", QualityText(g_doneIdx), QualityPoor(g_doneIdx) ? org : grn);
   if(g_compIdx >= 0)
     {
      Row("Composite", "POC " + Px(g_p[g_compIdx].poc) + " · VA " + Px(g_p[g_compIdx].val) + "-" + Px(g_p[g_compIdx].vah) + " · " + IntegerToString(g_compN) + " profiles · " + (g_p[g_compIdx].vtype == 1 ? "real volume" : "tick volume (approx.)") + (g_compMixEx > 0 ? " · " + IntegerToString(g_compMixEx) + " excluded (other type)" : ""), InpCompColor);
      if(g_effPeriod != VP_ROLL) Row("Data · composite", QualityText(g_compIdx), QualityPoor(g_compIdx) ? org : grn);
     }
   if(g_ancIdx >= 0)
     {
      string an = "";
      if(g_ancState == 2) an = " · anchor in a market gap: starts at the next candle (nothing lost)";
      else if(g_ancState == 3) an = " · SNAPPED forward to the next candle open: " + IntegerToString(g_ancLostSec / 60) + " min " + IntegerToString(g_ancLostSec % 60) + " s before it is NOT included (no intrabars)";
      else if(g_ancState == 4) an = " · anchor is before the available history: starts at the first loaded candle";
      else if(g_ancState == 6) an = " · cut exactly at the anchor using intrabars";
      Row("Anchored", "POC " + Px(g_p[g_ancIdx].poc) + " · VA " + Px(g_p[g_ancIdx].val) + "-" + Px(g_p[g_ancIdx].vah) + an, g_ancState == 3 || g_ancState == 4 ? org : InpAncColor);
      Row("Data · anchored", QualityText(g_ancIdx), QualityPoor(g_ancIdx) ? org : grn);
     }
   else if(InpAnc && g_ancState == 5)
      Row("Anchored", "NOT BUILT: the anchor time is after the newest completed candle", org);
   for(int s = 0; s < 4; s++)
     {
      int ms = g_msA[s];
      if(ms < 0 || g_p[ms].poc == EMPTY_VALUE) continue;
      Row(SlotName(s) + (g_msRun[s] ? "" : " (done)"), "POC " + Px(g_p[ms].poc) + " · H/L " + Px(g_p[ms].hi) + "/" + Px(g_p[ms].lo) + " · vol " + DoubleToString(g_p[ms].total, 0), SlotColor(s));
     }
   if(InpConf) Row("Confluence", IntegerToString(g_confZones) + " zone(s)", InpConfColor);
   int refC = g_curIdx >= 0 ? g_curIdx : g_doneIdx;
   string vam = InpVaMethod == VA_TV ? "TV rule" : (InpVaMethod == VA_TARGET1 ? "Target 1-row" : "Pair 2-row");
   Row("VA method", vam + " · " + DoubleToString(vVaPct, 0) + "%" + (refC >= 0 ? " → covers " + DoubleToString(g_p[refC].vaCov, 1) + "%" : ""), clrWhite);
   string note = "OK"; color nc = grn;
   if(vk != "Real volume") { note = "Tick volume ≠ real turnover"; nc = org; }
   if(g_effPeriod == VP_DAY && g_chartSec >= 86400) { note = "Chart TF ≥ period"; nc = red; }
   if(g_effPeriod == VP_WEEK && g_chartSec >= 604800) { note = "Chart TF ≥ period"; nc = red; }
   if(g_clamped > 0) { note = "Data outside grid: " + IntegerToString((int)g_clamped) + " candles clamped"; nc = org; }
   for(int k = 0; k < g_np; k++) if(!g_p[k].valid) { note = "Profile invalid (volume mismatch or data outside its grid)"; nc = red; }
   if(g_status != "") { note = g_status; nc = org; }
   if(g_inNote != "") Row("Inputs", "CORRECTED: " + g_inNote, clrOrange);
   Row("Note", note, nc);
  }

//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_CHART_CHANGE && InpShowPanel && g_rows > 0) PaintPanel();
  }

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 < 10) return 0;
   uint now = GetTickCount();
   datetime curBar = iTime(_Symbol, _Period, 0);                       // newest bar, independent of the direction of the input arrays
   bool newBar = (curBar != g_lastBar) || prev_calculated == 0;
   if(g_retry && now - g_lastTry > 3000) { newBar = true; }            // history / intrabar data was not ready: rebuild
   if(!newBar && now - g_lastPaint < 1500) return rates_total;
   if(newBar)
     {
      g_status = ""; g_retry = false; g_lastTry = now;
      CheckServerTz();
      if(!LoadChart()) { BuildPanel(); if(InpShowPanel) PaintPanel(); return 0; }
      if(g_strictNA) g_status = "Strict intrabars UNAVAILABLE (no usable lower timeframe): profiles are NOT built from chart bars";
      g_ltfBars = 0; g_fbBars = 0; g_skipBars = 0; g_cvExp = 0; g_cvGot = 0; g_cvSparse = 0; g_cvPartial = 0; g_cvTypeMis = 0; g_cvVolIn = 0; g_cvVolChart = 0;
      g_bufOff = rates_total - N;
      BuildProfiles();
      g_volKind = VolKindText();
      g_lastBar = curBar;
      CheckAlerts();
      PaintAll();
     }
   CheckLive(SymbolInfoDouble(_Symbol, SYMBOL_BID));
   BuildPanel();
   if(InpShowPanel) PaintPanel();
   PaintWatermark();
   g_lastPaint = now;
   ChartRedraw();
   return rates_total;
  }
//+------------------------------------------------------------------+
volume-profile-crypex.mq4 · MQL4 · ۲٬۵۸۸ خط
//+------------------------------------------------------------------+
//| Crypex | Volume Profile (MT4)                                    |
//| POC / Value Area / HVN-LVN / naked POC with intrabar volume     |
//| © 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 description "Volume Profile: POC, Value Area, HVN/LVN, naked POC, developing lines, status panel"
#property strict
#property indicator_chart_window
#property indicator_buffers 3
#property indicator_color1  C'255,152,0'
#property indicator_width1  2
#property indicator_color2  C'41,98,255'
#property indicator_color3  C'41,98,255'

enum ENUM_VP_PERIOD { VP_AUTO, VP_DAY, VP_WEEK, VP_MONTH, VP_SESSION, VP_ROLL };
enum ENUM_VP_VA     { VA_TV, VA_TARGET1, VA_PAIR2 };
enum ENUM_VP_VSRC   { SRC_FALLBACK, SRC_STRICT, SRC_CHART };
enum ENUM_VP_SESSBND { SESSB_EXCLUDE, SESSB_ESTIMATE };
enum ENUM_VP_TZ     { TZ_SERVER, TZ_UTC, TZ_LONDON, TZ_NEWYORK, TZ_TOKYO, TZ_FIXED };
enum ENUM_VP_DST    { DST_NONE, DST_US, DST_EU, DST_MANUAL };
enum ENUM_VP_WEEK   { WK_AUTO, WK_MONDAY, WK_SUNDAY };
enum ENUM_VP_NODEMODE { NODE_ADV, NODE_SIMPLE };
enum ENUM_VP_TOUCH    { NT_WICK, NT_CLOSE };
enum ENUM_VP_COMP     { COMP_LASTN, COMP_WEEK, COMP_MONTH };
enum ENUM_VP_ALERT  { AL_CLOSE, AL_LIVE };
enum ENUM_VP_ROWS   { ROWS_AUTO, ROWS_FIXED, ROWS_COUNT };
enum ENUM_VP_DIR    { DIR_LEFT_RIGHT, DIR_RIGHT_LEFT };
enum ENUM_PANEL_POS { POS_TOP_RIGHT, POS_TOP_LEFT, POS_BOTTOM_RIGHT, POS_BOTTOM_LEFT };

// ---- Profile
input ENUM_VP_PERIOD InpPeriod   = VP_AUTO;        // Profile period (Auto picks Day/Week/Month/Last N bars by chart TF; or Day/Week/Month/Session/Last N bars)
input string InpSessStart  = "08:00";              // Session start (in the chosen time basis, Session mode)
input string InpSessEnd    = "16:00";              // Session end (in the chosen time basis, Session mode)
input ENUM_VP_TZ InpTz     = TZ_SERVER;            // Time basis for Day / Week / Month / Session boundaries
input double InpServerOffset = 2.0;                // Broker server UTC offset in WINTER (hours), used when the basis is not Server
input ENUM_VP_DST InpServerDst = DST_US;           // Broker server DST schedule (US = most brokers, EU, none, or the manual schedule below)
input string InpOffsetSched = "";                  // Manual schedule: "2025.03.30 01:00=3;2025.10.26 01:00=2" = from that UTC instant the server is at UTC+offset
input double InpFixedOffset = 0.0;                 // UTC offset (hours) for the "Fixed" time basis
input ENUM_VP_WEEK InpWeekStart = WK_AUTO;         // Week start (Auto: Monday on 24/7 symbols, Sunday otherwise)
input int    InpRollN      = 300;                  // N bars (Last N bars mode, chart-bar data)
input int    InpKeepPrev   = 2;                    // Previous profiles to show
input int    InpRows       = 70;                   // Rows per profile
input ENUM_VP_ROWS InpRowMode = ROWS_AUTO;         // Row size (Auto = avg range of previous 5 profiles / rows; Fixed ticks; Row count = this profile's own range / rows, tick-aligned)
input int    InpRowTicks   = 10;                   // Fixed row size (ticks)
input double InpVaPct      = 70.0;                 // Value Area %
input ENUM_VP_VA InpVaMethod = VA_TV;              // Value Area method (TradingView documented / Target-reached 1 row / Pair 2 rows)
input ENUM_TIMEFRAMES InpLtf = PERIOD_CURRENT;     // Intrabar timeframe (current = Auto)
input ENUM_VP_VSRC InpVolSrc = SRC_FALLBACK;       // Volume source (Fallback to chart bar / Strict intrabars / Chart bars only)
input ENUM_VP_SESSBND InpSessBound = SESSB_EXCLUDE; // Session boundary candle without intrabars: Exclude (safe) or Estimate (prorated, flagged)
input double InpVolTol     = 2.0;                  // Intrabar volume tolerance (%) for the fallback check
input double InpTickOverride = 0.0;                // Price tick size override (0 = from symbol)
input int    InpMaxBars    = 3000;                 // Chart bars to analyse
input int    InpNakedLook  = 30;                   // Older profiles scanned for naked POC
// ---- Histogram
input bool   InpShowHist   = true;
input ENUM_VP_DIR InpDir   = DIR_LEFT_RIGHT;       // Direction
input int    InpWidthPct   = 35;                   // Width (% of period length)
input bool   InpSplitUD    = true;                 // Split Up / Down volume
input color  InpUpColor    = C'8,153,129';
input color  InpDnColor    = C'242,54,69';
input color  InpVaColor    = C'41,98,255';        // Inside VA (single colour)
input color  InpOutColor   = C'120,123,134';      // Outside VA
input color  InpPocRowColor = C'255,152,0';
input int    InpInTransp   = 35;                   // Inside-VA transparency (0-95)
input int    InpOutTransp  = 70;                   // Outside-VA transparency (0-95)
// ---- Levels
input bool   InpShowPoc    = true;
input color  InpPocColor   = C'255,152,0';
input int    InpPocWidth   = 2;
input bool   InpShowVA     = true;                 // VAH / VAL lines
input color  InpVaLineColor = C'41,98,255';
input int    InpVaLineWidth = 1;
input bool   InpShadeVA    = true;
input bool   InpExtendPrev = true;                 // Extend the latest finished profile into the current period
input int    InpLineExt    = 10;                   // Extend lines into the future (bars)
input bool   InpShowLabels = true;
input int    InpLabelSize  = 8;                    // Price label font size
input bool   InpShowDev    = false;                // Developing POC / VA lines
// ---- Nodes (HVN / LVN)
input bool   InpNodes      = true;                 // Mark HVN / LVN
input ENUM_VP_NODEMODE InpNodeMode = NODE_ADV;     // Advanced (prominence, separation, zones) or Simple (legacy local extrema)
input int    InpNodeProfs  = 2;                    // Profiles with nodes (newest)
input double InpNodeSmoothPct = 2.0;               // Advanced: smoothing half-width (% of rows, min 1 row)
input double InpNodeProm   = 15.0;                 // Advanced: min prominence (% of the highest smoothed row)
input double InpNodeSep    = 8.0;                  // Advanced: min separation between HVNs (% of rows)
input double InpNodeZonePct = 70.0;                // Advanced: zone = rows around the peak above this % of the peak
input bool   InpNodeZones  = true;                 // Advanced: shade node zones
input double InpNodeWinPct = 5.0;                  // Simple: window (% of rows, each side, min 2 rows)
input double InpHvnPct     = 40.0;                 // HVN: min % of the highest (smoothed) row
input double InpLvnPct     = 50.0;                 // LVN: max % of the highest (smoothed) row
input color  InpHvnColor   = C'0,188,212';
input color  InpLvnColor   = C'233,30,99';
// ---- Naked POC
input bool   InpNaked      = true;                 // Naked POC (not yet revisited)
input int    InpNakedMax   = 6;                    // Max active naked POCs shown
input ENUM_VP_TOUCH InpNakedTouch = NT_WICK;       // Revisit rule: any wick touch, or a close through the level
input int    InpNakedMaxAge = 0;                   // Hide naked POCs older than N bars (0 = never)
input int    InpNakedConsumed = 0;                 // Also show the last N consumed (touched) POCs, faded
input bool   InpNakedProv  = false;                // Flag a wick touch by the forming bar as "provisional" (never changes the confirmed state)
input color  InpNakedColor = C'255,152,0';
// ---- Analytics (panel)
input bool   InpMigration  = true;                 // POC migration vs previous profile
input double InpMigTol     = 1.0;                  // POC counts as stable within this many rows
input bool   InpVaShift    = true;                 // Value Area shift vs previous profile
input bool   InpShape      = false;                // Profile shape (heuristic: D / P / b / double distribution)
input bool   InpRvol       = false;                // Relative volume vs the same elapsed time in previous profiles
input int    InpRvolN      = 5;                    // Previous profiles averaged for relative volume
// ---- Initial Balance
input bool   InpIB         = false;                // Initial Balance of the profile period (best with Session mode)
input int    InpIBMin      = 60;                   // IB duration (minutes from the period start)
input bool   InpIBExt      = true;                 // Show 1x and 2x range extensions
input color  InpIBColor    = C'171,71,188';
// ---- Confluence zones
input bool   InpConf       = false;                // Mark places where several key levels overlap
input double InpConfTol    = 0.25;                 // Cluster tolerance (x ATR14)
input int    InpConfMin    = 3;                    // Min number of levels in a zone
input int    InpConfMax    = 4;                    // Max zones shown (nearest to price)
input color  InpConfColor  = C'255,214,0';
// ---- Composite profile
input bool   InpComp       = false;                // Composite of several profiles (no double counting)
input ENUM_VP_COMP InpCompMode = COMP_LASTN;       // Last N profiles / current week / current month
input int    InpCompN      = 5;                    // N (Last N profiles mode; the running profile counts as one)
input int    InpCompRows   = 70;                   // Composite rows
input bool   InpCompHist   = false;                // Composite histogram (right side)
input color  InpCompColor  = C'0,150,136';
// ---- Anchored profile
input bool   InpAnc        = false;                // Profile from a chosen date/time to now (uses intrabars)
input datetime InpAncTime  = 0;                    // Anchor date/time (broker server time)
input int    InpAncRows    = 70;                   // Anchored rows
input bool   InpAncHist    = false;                // Anchored histogram
input color  InpAncColor   = C'255,112,67';
// ---- Multi-session profiles
input bool   InpMsHist     = false;                // Histograms for session profiles (default: lines only)
input bool   InpMs1On      = false;                // Session 1 (Asia)
input string InpMs1Start   = "00:00";
input string InpMs1End     = "09:00";
input ENUM_VP_TZ InpMs1Tz  = TZ_TOKYO;
input color  InpMs1Color   = C'171,71,188';
input bool   InpMs2On      = false;                // Session 2 (London)
input string InpMs2Start   = "08:00";
input string InpMs2End     = "16:30";
input ENUM_VP_TZ InpMs2Tz  = TZ_LONDON;
input color  InpMs2Color   = C'41,98,255';
input bool   InpMs3On      = false;                // Session 3 (New York)
input string InpMs3Start   = "09:30";
input string InpMs3End     = "16:00";
input ENUM_VP_TZ InpMs3Tz  = TZ_NEWYORK;
input color  InpMs3Color   = C'38,166,91';
input bool   InpMs4On      = false;                // Session 4 (custom)
input string InpMs4Start   = "13:00";
input string InpMs4End     = "17:00";
input ENUM_VP_TZ InpMs4Tz  = TZ_UTC;
input color  InpMs4Color   = C'255,152,0';
// ---- Alerts
input ENUM_VP_ALERT InpAlertMode = AL_CLOSE;       // Level alerts: Confirmed close (candle closed beyond the level) / Live cross (price crossed now, may repeat)
input bool   InpAlertBlockPoor = true;              // Do NOT alert on a previous profile whose data is incomplete (missing/partial intrabars, chart-bar fallback, low coverage)
input bool   InpAlertNewPeriod = false;            // Alert when a new period starts (the previous profile levels change)
input bool   InpAlertHealth = true;                // Alert when the data/time basis becomes unreliable (Strict unavailable, server-time mismatch, incomplete previous profile)
input bool   InpAlertPopup = true;
input bool   InpAlertPush  = false;
input bool   InpAlertMail  = false;
// ---- Status panel
input bool   InpShowPanel  = false;                // Off by default: turn on from the input settings if you want the status table
input ENUM_PANEL_POS InpPanelPos = POS_TOP_LEFT;   // Top-left (default): the profile/histogram/labels are drawn near the current price and time (right side), so the panel stays clear of them there
input int    InpPanelFont  = 7;
input bool   InpShowTickRow = false;               // Tick size diagnostic row (debugging only)
input color  InpPanelBg    = C'21,26,32';
// ---- Watermark
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)

string PFX = "CRX_VP_";                 // made unique per indicator instance in OnInit
const color  BRAND_GREEN = C'11,163,28';

double BufPoc[], BufVah[], BufVal[];

//---- chart bars (chronological, index 0 = oldest)
datetime cTm[];
double   cHi[], cLo[], cOp[], cCl[], cVolR[], cVolT[];
int      N = 0;
int      g_chartSec = 60;
ENUM_VP_PERIOD g_effPeriod = VP_DAY;    // resolved period: InpPeriod, or (if Auto) the period picked from the chart's own timeframe

//---- lower timeframe bars (chronological)
datetime lT[];
double   lH[], lL[], lVr[], lVt[];
bool     lU[];
int      lN = 0;
ENUM_TIMEFRAMES g_ltf = PERIOD_CURRENT;
bool     g_useLtf = false;
bool     g_strictNA = false;                     // CRX-009: Strict requested but no lower timeframe is usable

//---- profiles (scalar struct; rows are stored in flat arrays)
struct SProf
  {
   int      i0, i1;            // first / last chart bar index
   datetime t0, t1;
   double   rs, off;
   int      base;              // row index of the first stored row
   int      rowStart, rowN;
   double   total, upTotal, hi, lo;
   double   poc, vah, val;
   int      pocI, vaLo, vaHi;
   double   vaCov;             // share of the profile volume inside [VAL, VAH] (%)
   int      vtype;             // 1 = real volume, 0 = tick volume
   bool     valid;             // row volumes add up to the total
   bool     naked;             // POC not revisited (confirmed on closed bars only)
   bool     prov;              // the forming bar is touching the POC right now (provisional, not part of the confirmed state)
   int      touchBar;          // first chart bar that revisited the POC (-1 = none)
   int      kind;              // 0 primary, 1 composite, 2 anchored, 3 session slot
   int      slot;              // session slot (0..3)
   int      cumStart, cumN;    // cumulative chart-bar volume per bar since the profile start (relative volume)
   double   ibH, ibL;          // Initial Balance (primary profiles)
   long     qIntra, qFb, qSkip, qExp, qGot, qSparse, qPartial, qTypeMis;   // CRX-017: data quality of THIS profile
   double   qVolIn, qVolChart;
   bool     ibEst;             // CRX-012: IB lost a partially-covered candle (no intrabars) -> approximate
   int      bPrecise, bEstimated, bExcluded;   // session-boundary candle quality: precise (intrabar-filtered), estimated (prorated), excluded
   int      clamped;           // CRX-003: data that fell outside this profile's own grid (should be ~0; >0 makes the profile invalid)
  };
SProf  g_p[];
int    g_np = 0;               // PRIMARY profiles (indexes 0..g_np-1); extra profiles follow
double g_up[], g_dn[];         // flat row volumes of all profiles
double g_cum[];                // flat cumulative volumes
int    g_compIdx = -1, g_ancIdx = -1;
int    g_msA[4], g_msB[4];     // session slots: latest session and the one before (-1 = none)
bool   g_msRun[4];             // the latest session of the slot is still running
int    g_tot = 0;              // all profiles
bool   g_sessOK = false;
int    g_sessS = 0, g_sessE = 0;
datetime g_lastBar = 0;
datetime g_lastAlertBar = 0;
uint   g_lastPaint = 0;
long   g_ltfBars = 0, g_fbBars = 0, g_skipBars = 0, g_clamped = 0;
long   g_cvExp = 0, g_cvGot = 0, g_cvSparse = 0, g_cvPartial = 0, g_cvTypeMis = 0;   // CRX-008: intrabar coverage accounting
double g_cvVolIn = 0, g_cvVolChart = 0;
int    g_curIdx = -1, g_doneIdx = -1;     // current (running) profile and latest FINISHED profile; -1 = none
string g_status = "";
bool   g_retry = false;
uint   g_lastTry = 0;
bool   g_alertInit = false;
double g_livePx = 0;
datetime g_liveBar[6];
int    g_healthSeen = 0;
bool   g_weekSun = false;
double g_tick = 0.0;
double g_tickRaw = 0.0;
string g_tickSrc = "";
int    g_nReal = 0, g_nTick = 0;
int    g_ancState = 0, g_ancLostSec = 0;                  // CRX-016: 1 exact, 2 gap->next candle, 3 snapped fwd (lost sec), 4 before history, 5 after last candle, 6 cut by intrabars
int    g_compN = 0;
int    g_compMixEx = 0, g_compCut = 0, g_realEmpty = 0;                 // CRX-010: composite sources excluded for a different volume type / Real-only profiles without volume
string g_volKind = "Tick volume (approx.)";

//+------------------------------------------------------------------+
bool IsCryptoSymbol()
  {
   string s = _Symbol;
   string keys[] = {"BTC", "ETH", "LTC", "XRP", "SOL", "DOGE", "ADA", "BNB", "TRX", "DOT"};
   for(int i = 0; i < ArraySize(keys); i++)
      if(StringFind(s, keys[i]) >= 0) return true;
   return false;
  }

// CRX-027: validated copies of numeric inputs (input variables are read-only); filled by ValidateInputs() at start of OnInit
int vRows;
int vRowTicks;
double vVaPct;
double vVolTol;
int vMaxBars;
int vRollN;
int vKeepPrev;
int vNakedLook;
int vNakedMax;
int vCompN;
int vCompRows;
int vAncRows;
int vRvolN;
int vIBMin;
double vConfTol;
int vConfMin;
int vConfMax;
double vNodeSmoothPct;
double vNodeProm;
double vNodeSep;
double vNodeZonePct;
double vNodeWinPct;
double vHvnPct;
double vLvnPct;
int vWidthPct;
int vInTransp;
int vOutTransp;
int vLineExt;
int vPocWidth;
int vVaLineWidth;
int vLabelSize;
int vPanelFont;
double vMigTol;
int vNodeProfs;
double vFixedOffset;
double vServerOffset;
int vNakedConsumed;
string g_inNote = "";
void AddNote(const string t)
  {
   if(g_inNote != "") g_inNote += ", ";
   g_inNote += t;
  }
void ClampI(int &dst, const int v, const int lo, const int hi, const string name)
  {
   int r = v < lo ? lo : (v > hi ? hi : v);
   if(r != v) AddNote(name + " " + IntegerToString(v) + "->" + IntegerToString(r));
   dst = r;
  }
void ClampD(double &dst, const double v, const double lo, const double hi, const string name)
  {
   double r = !MathIsValidNumber(v) ? lo : (v < lo ? lo : (v > hi ? hi : v));
   if(r != v) AddNote(name + " " + DoubleToString(v, 2) + "->" + DoubleToString(r, 2));
   dst = r;
  }
void ValidateInputs()
  {
   g_inNote = "";
   ClampI(vRows, InpRows, 2, 2000, "Rows");
   ClampI(vRowTicks, InpRowTicks, 1, 100000, "RowTicks");
   ClampD(vVaPct, InpVaPct, 1, 100, "VaPct");
   ClampD(vVolTol, InpVolTol, 0, 100, "VolTol");
   ClampI(vMaxBars, InpMaxBars, 100, 100000, "MaxBars");
   ClampI(vRollN, InpRollN, 2, 20000, "RollN");
   ClampI(vKeepPrev, InpKeepPrev, 0, 50, "KeepPrev");
   ClampI(vNakedLook, InpNakedLook, 0, 500, "NakedLook");
   ClampI(vNakedMax, InpNakedMax, 0, 50, "NakedMax");
   ClampI(vCompN, InpCompN, 1, 200, "CompN");
   ClampI(vCompRows, InpCompRows, 2, 2000, "CompRows");
   ClampI(vAncRows, InpAncRows, 2, 2000, "AncRows");
   ClampI(vRvolN, InpRvolN, 1, 50, "RvolN");
   ClampI(vIBMin, InpIBMin, 1, 1440, "IBMin");
   ClampD(vConfTol, InpConfTol, 0.01, 5, "ConfTol");
   ClampI(vConfMin, InpConfMin, 2, 10, "ConfMin");
   ClampI(vConfMax, InpConfMax, 1, 10, "ConfMax");
   ClampD(vNodeSmoothPct, InpNodeSmoothPct, 0, 20, "NodeSmoothPct");
   ClampD(vNodeProm, InpNodeProm, 0, 100, "NodeProm");
   ClampD(vNodeSep, InpNodeSep, 0, 50, "NodeSep");
   ClampD(vNodeZonePct, InpNodeZonePct, 1, 100, "NodeZonePct");
   ClampD(vNodeWinPct, InpNodeWinPct, 0, 50, "NodeWinPct");
   ClampD(vHvnPct, InpHvnPct, 0, 100, "HvnPct");
   ClampD(vLvnPct, InpLvnPct, 0, 100, "LvnPct");
   ClampI(vWidthPct, InpWidthPct, 1, 100, "WidthPct");
   ClampI(vInTransp, InpInTransp, 0, 95, "InTransp");
   ClampI(vOutTransp, InpOutTransp, 0, 95, "OutTransp");
   ClampI(vLineExt, InpLineExt, 0, 500, "LineExt");
   ClampI(vPocWidth, InpPocWidth, 1, 5, "PocWidth");
   ClampI(vVaLineWidth, InpVaLineWidth, 1, 5, "VaLineWidth");
   ClampI(vLabelSize, InpLabelSize, 5, 24, "LabelSize");
   ClampI(vPanelFont, InpPanelFont, 6, 24, "PanelFont");
   ClampD(vMigTol, InpMigTol, 0, 10, "MigTol");
   ClampI(vNodeProfs, InpNodeProfs, 0, 10, "NodeProfs");
   ClampD(vFixedOffset, InpFixedOffset, -14, 14, "FixedOffset");
   ClampD(vServerOffset, InpServerOffset, -14, 14, "ServerOffset");
   ClampI(vNakedConsumed, InpNakedConsumed, 0, 50, "NakedConsumed");
  }

int ParseHM(const string s)
  {
   string p[];
   if(StringSplit(s, ':', p) < 2) { AddNote("time '" + s + "' invalid->00:00"); return 0; }
   int h = (int)StringToInteger(p[0]), m = (int)StringToInteger(p[1]);
   if(h < 0 || h > 23 || m < 0 || m > 59) { AddNote("time '" + s + "' out of range->00:00"); return 0; }
   return h * 60 + m;
  }

ENUM_TIMEFRAMES AutoLtf(const int sec)
  {
   if(sec <= 60) return PERIOD_CURRENT;
   if(sec / 60 <= 100)   return PERIOD_M1;
   if(sec / 300 <= 100)  return PERIOD_M5;
   if(sec / 900 <= 100)  return PERIOD_M15;
   if(sec / 1800 <= 100) return PERIOD_M30;
   if(sec / 3600 <= 100) return PERIOD_H1;
   return PERIOD_H4;
  }

string Px(const double v)
  {
   if(v == EMPTY_VALUE || v != v) return "—";
   return DoubleToString(v, _Digits);
  }

color Blend(const color c, const color bg, const int transp)
  {
   double a = 1.0 - transp / 100.0;
   int ci = (int)c, bi = (int)bg;
   int r1 = ci & 0xFF, g1 = (ci >> 8) & 0xFF, b1 = (ci >> 16) & 0xFF;
   int r2 = bi & 0xFF, g2 = (bi >> 8) & 0xFF, b2 = (bi >> 16) & 0xFF;
   int r = (int)MathRound(r1 * a + r2 * (1.0 - a)), g = (int)MathRound(g1 * a + g2 * (1.0 - a)), b = (int)MathRound(b1 * a + b2 * (1.0 - a));
   return (color)(r | (g << 8) | (b << 16));
  }

//+------------------------------------------------------------------+
//| Time basis (UTC / London / New York / Tokyo with DST)            |
//+------------------------------------------------------------------+
long DaysFromCivil(int y, const int m, const int d)
  {
   if(m <= 2) y--;
   long era = (y >= 0 ? y : y - 399) / 400;
   long yoe = y - era * 400;
   long doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
   long doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
   return era * 146097 + doe - 719468;
  }

int WeekdayOfDay(const long days)                        // 0 = Sunday
  {
   long w = (days + 4) % 7;
   if(w < 0) w += 7;
   return (int)w;
  }

long NthSundayDay(const int y, const int m, const int n) // n-th Sunday of the month (1-based), as a day number
  {
   long d1 = DaysFromCivil(y, m, 1);
   long first = d1 + ((7 - WeekdayOfDay(d1)) % 7);
   return first + (long)(n - 1) * 7;
  }

long LastSundayDay(const int y, const int m)
  {
   int ny = (m == 12) ? y + 1 : y, nm = (m == 12) ? 1 : m + 1;
   long last = DaysFromCivil(ny, nm, 1) - 1;
   return last - WeekdayOfDay(last);
  }

bool IsDstEU(const datetime utc)                         // last Sunday of March 01:00 UTC -> last Sunday of October 01:00 UTC
  {
   MqlDateTime d;
   TimeToStruct(utc, d);
   datetime a = (datetime)(LastSundayDay(d.year, 3) * 86400 + 3600);
   datetime b = (datetime)(LastSundayDay(d.year, 10) * 86400 + 3600);
   return utc >= a && utc < b;
  }

bool IsDstUS(const datetime utc)                         // 2nd Sunday of March 07:00 UTC -> 1st Sunday of November 06:00 UTC
  {
   MqlDateTime d;
   TimeToStruct(utc, d);
   datetime a = (datetime)(NthSundayDay(d.year, 3, 2) * 86400 + 7 * 3600);
   datetime b = (datetime)(NthSundayDay(d.year, 11, 1) * 86400 + 6 * 3600);
   return utc >= a && utc < b;
  }

// CRX-013: manual server-offset schedule. Entries: "yyyy.mm.dd hh:mi=offsetHours" separated by ';', times in UTC, sorted on load.
// Before the first entry InpServerOffset applies. The repeated hour at a fall-back is resolved to its FIRST occurrence.
datetime g_schT[];
double   g_schO[];
int      g_schN = 0;

void ParseSchedule()
  {
   g_schN = 0;
   ArrayResize(g_schT, 0); ArrayResize(g_schO, 0);
   if(InpServerDst != DST_MANUAL) return;
   string parts[];
   int np = StringSplit(InpOffsetSched, ';', parts);
   for(int i = 0; i < np; i++)
     {
      string kv[];
      if(StringSplit(parts[i], '=', kv) != 2) continue;
      datetime t = StringToTime(kv[0]);
      if(t <= 0) continue;
      int n = g_schN;
      ArrayResize(g_schT, n + 1); ArrayResize(g_schO, n + 1);
      int pos = n;
      while(pos > 0 && g_schT[pos - 1] > t) { g_schT[pos] = g_schT[pos - 1]; g_schO[pos] = g_schO[pos - 1]; pos--; }
      g_schT[pos] = t; g_schO[pos] = StringToDouble(kv[1]);
      g_schN++;
     }
  }

double ManualOffsetAtServer(const datetime t)           // server offset (hours) in force at SERVER time t
  {
   double off = vServerOffset;
   for(int i = 0; i < g_schN; i++)
     {
      if(t >= g_schT[i] + (datetime)MathRound(off * 3600.0)) off = g_schO[i]; else break;
     }
   return off;
  }

datetime ServerToUtc(const datetime t)
  {
   if(InpServerDst == DST_MANUAL) return t - (datetime)MathRound(ManualOffsetAtServer(t) * 3600.0);
   long base = (long)MathRound(vServerOffset * 3600.0);
   if(InpServerDst != DST_NONE)
     {
      datetime u = t - (datetime)(base + 3600);                    // reading "the server is on summer time"
      if((InpServerDst == DST_US && IsDstUS(u)) || (InpServerDst == DST_EU && IsDstEU(u))) return u;
     }
   return t - (datetime)base;                                      // otherwise winter time (the repeated hour at the DST end is ambiguous by nature)
  }

datetime ToLocalTz(const datetime t, const ENUM_VP_TZ tz)
  {
   if(tz == TZ_SERVER) return t;
   datetime utc = ServerToUtc(t);
   if(tz == TZ_UTC) return utc;
   if(tz == TZ_LONDON)  return utc + (IsDstEU(utc) ? 3600 : 0);
   if(tz == TZ_NEWYORK) return utc + (IsDstUS(utc) ? -4 * 3600 : -5 * 3600);
   if(tz == TZ_TOKYO)   return utc + 9 * 3600;
   return utc + (datetime)MathRound(vFixedOffset * 3600.0);
  }

// CRX-013: the offset model (InpServerOffset + DST rule / manual schedule) is an ASSUMPTION about the broker. When the terminal's own
// GMT clock is available, compare the two: 1 = model agrees with the terminal now, 2 = MISMATCH, 0 = cannot be judged (stale or odd clock).
int    g_tzState = 0;
double g_tzObs = 0, g_tzMdl = 0;
void CheckServerTz()
  {
   g_tzState = 0;
   datetime ts = TimeTradeServer(), gmt = TimeGMT();
   if(ts <= 0 || gmt <= 0) return;
   long obs = (long)(ts - gmt), mdl = (long)(ts - ServerToUtc(ts));
   long q = (long)MathRound(obs / 900.0) * 900;
   if(MathAbs((double)(obs - q)) > 120.0 || MathAbs((double)obs) > 14 * 3600) return;           // not a clean quarter-hour offset: stale tick / clock skew
   g_tzObs = obs / 3600.0; g_tzMdl = mdl / 3600.0;
   g_tzState = (MathAbs((double)(q - mdl)) < 900.0) ? 1 : 2;
  }

bool UsesTzConversion()
  {
   if(InpTz != TZ_SERVER) return true;
   if(InpMs1On && InpMs1Tz != TZ_SERVER) return true;
   if(InpMs2On && InpMs2Tz != TZ_SERVER) return true;
   if(InpMs3On && InpMs3Tz != TZ_SERVER) return true;
   if(InpMs4On && InpMs4Tz != TZ_SERVER) return true;
   return false;
  }

datetime ToLocal(const datetime t)
  {
   return ToLocalTz(t, InpTz);
  }

// period id of a candle open time for a period kind (Day / Week / Month / Session); -1 = outside the session
long PeriodIdKind(const ENUM_VP_PERIOD kind, const datetime t)
  {
   datetime tl = ToLocal(t);
   MqlDateTime d;
   TimeToStruct(tl, d);
   long days = (long)(tl / 86400);
   if(kind == VP_DAY)   return days;
   if(kind == VP_WEEK)  return g_weekSun ? (days + 4) / 7 : (days + 3) / 7;                  // 1970-01-01 was a Thursday
   if(kind == VP_MONTH) return (long)d.year * 12 + d.mon;
   int m = d.hour * 60 + d.min;                                                              // session
   if(g_sessS < g_sessE) return (m >= g_sessS && m < g_sessE) ? days : -1;
   if(m >= g_sessS) return days;
   if(m < g_sessE)  return days - 1;
   return -1;
  }

long PeriodId(const datetime t)
  {
   return PeriodIdKind(g_effPeriod, t);
  }

//+------------------------------------------------------------------+
//| Session slots (multi-session profiles)                           |
//+------------------------------------------------------------------+
int g_slotS[4], g_slotE[4];

bool SlotOn(const int s)
  {
   if(s == 0) return InpMs1On;
   if(s == 1) return InpMs2On;
   if(s == 2) return InpMs3On;
   return InpMs4On;
  }
string SlotName(const int s)
  {
   if(s == 0) return "Asia";
   if(s == 1) return "London";
   if(s == 2) return "New York";
   return "Custom";
  }
ENUM_VP_TZ SlotTz(const int s)
  {
   if(s == 0) return InpMs1Tz;
   if(s == 1) return InpMs2Tz;
   if(s == 2) return InpMs3Tz;
   return InpMs4Tz;
  }
color SlotColor(const int s)
  {
   if(s == 0) return InpMs1Color;
   if(s == 1) return InpMs2Color;
   if(s == 2) return InpMs3Color;
   return InpMs4Color;
  }
string SlotStartText(const int s)
  {
   if(s == 0) return InpMs1Start;
   if(s == 1) return InpMs2Start;
   if(s == 2) return InpMs3Start;
   return InpMs4Start;
  }
string SlotEndText(const int s)
  {
   if(s == 0) return InpMs1End;
   if(s == 1) return InpMs2End;
   if(s == 2) return InpMs3End;
   return InpMs4End;
  }

// session id of a candle open time for slot s (-1 = outside the session)
long SlotId(const datetime t, const int s)
  {
   datetime tl = ToLocalTz(t, SlotTz(s));
   MqlDateTime d;
   TimeToStruct(tl, d);
   long days = (long)(tl / 86400);
   int m = d.hour * 60 + d.min, S = g_slotS[s], E = g_slotE[s];
   if(S < E) return (m >= S && m < E) ? days : -1;
   if(m >= S) return days;
   if(m < E)  return days - 1;
   return -1;
  }

// filt: 0 = primary period definition, 1..4 = session slot
long IdOf(const datetime t, const int filt)
  {
   if(filt == 200) return t >= InpAncTime ? -2 : -1;          // CRX-016: anchored profile - everything from the anchor instant on
   return filt == 0 ? PeriodId(t) : SlotId(t, filt - 1);
  }

// CRX-017: data-quality text of ONE profile (so an old, poorly covered profile is not hidden by a good global average)
string QualityText(const int idx)
  {
   if(idx < 0) return "";
   long n = g_p[idx].qIntra + g_p[idx].qFb + g_p[idx].qSkip;
   if(n <= 0) return g_useLtf && g_effPeriod != VP_ROLL ? "no candle data" : "chart bars";
   string t = IntegerToString((int)(g_p[idx].qIntra * 100 / n)) + "% intrabar";
   if(g_p[idx].qExp > 0) t += " · time " + DoubleToString(100.0 * g_p[idx].qGot / g_p[idx].qExp, 1) + "%";
   if(g_p[idx].qVolChart > 0) t += " · vol " + DoubleToString(100.0 * g_p[idx].qVolIn / g_p[idx].qVolChart, 1) + "%";
   if(g_p[idx].qFb > 0) t += " · " + IntegerToString((int)g_p[idx].qFb) + " chart-bar";
   if(g_p[idx].qSkip > 0) t += " · " + IntegerToString((int)g_p[idx].qSkip) + " MISSING";
   if(g_p[idx].qPartial > 0) t += " · " + IntegerToString((int)g_p[idx].qPartial) + " PARTIAL";
   if(g_p[idx].qTypeMis > 0) t += " · " + IntegerToString((int)g_p[idx].qTypeMis) + " vol-type mismatch";
   return t;
  }

bool QualityPoor(const int idx)
  {
   if(idx < 0) return false;
   return g_p[idx].qSkip > 0 || g_p[idx].qPartial > 0 || g_p[idx].qTypeMis > 0 || (g_p[idx].qFb > 0) ||
          (g_p[idx].qExp > 0 && g_p[idx].qGot < g_p[idx].qExp * 0.9) || (g_p[idx].qVolChart > 0 && g_p[idx].qVolIn < g_p[idx].qVolChart * 0.95);
  }

//+------------------------------------------------------------------+
int OnInit()
  {
   PFX = "CRX_VP" + IntegerToString((int)(GetMicrosecondCount() % 1000000)) + "_";          // unique per instance
   IndicatorBuffers(3);
   SetIndexBuffer(0, BufPoc);
   SetIndexBuffer(1, BufVah);
   SetIndexBuffer(2, BufVal);
   ArraySetAsSeries(BufPoc, true);                      // explicit contract: index 0 = newest bar (shift); every read/write below uses bar shift
   ArraySetAsSeries(BufVah, true);
   ArraySetAsSeries(BufVal, true);
   SetIndexStyle(0, InpShowDev ? DRAW_LINE : DRAW_NONE, STYLE_SOLID, 2, InpPocColor);
   SetIndexStyle(1, InpShowDev ? DRAW_LINE : DRAW_NONE, STYLE_SOLID, 1, InpVaLineColor);
   SetIndexStyle(2, InpShowDev ? DRAW_LINE : DRAW_NONE, STYLE_SOLID, 1, InpVaLineColor);
   SetIndexLabel(0, "Developing POC");
   SetIndexLabel(1, "Developing VAH");
   SetIndexLabel(2, "Developing VAL");
   for(int p = 0; p < 3; p++) SetIndexEmptyValue(p, EMPTY_VALUE);

   ValidateInputs();
   ParseSchedule();
   g_chartSec = PeriodSeconds(_Period);
   // Auto: picks the period that always has finer candles inside it than the chart itself, so the
   // "Chart TF ≥ period" guard never blanks the indicator, no matter what timeframe it is attached to.
   g_effPeriod = (InpPeriod != VP_AUTO) ? InpPeriod : (g_chartSec < 86400 ? VP_DAY : g_chartSec < 604800 ? VP_WEEK : g_chartSec < 2592000 ? VP_MONTH : VP_ROLL);
   g_sessS = ParseHM(InpSessStart);
   g_sessE = ParseHM(InpSessEnd);
   for(int q = 0; q < 4; q++) { g_slotS[q] = ParseHM(SlotStartText(q)); g_slotE[q] = ParseHM(SlotEndText(q)); }
   g_ltf = (InpLtf == PERIOD_CURRENT) ? AutoLtf(g_chartSec) : InpLtf;
   g_useLtf = (g_effPeriod != VP_ROLL) && InpVolSrc != SRC_CHART && g_ltf != PERIOD_CURRENT && PeriodSeconds(g_ltf) < g_chartSec;
   g_strictNA = (InpVolSrc == SRC_STRICT && g_effPeriod != VP_ROLL && !g_useLtf && g_chartSec > 60);
   //---- CRX-002: price tick size. MQL4 documents MODE_TICKSIZE as "tick size in points" (docs.mql4.com,
   //---- Market Info constants), so the documented, non-ambiguous conversion is MODE_TICKSIZE * Point - no
   //---- guessing from the symbol's digits or from the raw value's magnitude. If a broker's build genuinely
   //---- returns MODE_TICKSIZE already in price units (a known, undocumented deviation on some servers), use
   //---- InpTickOverride; the panel's "Tick size (eff.)" row always shows what was actually used.
   double tkRaw = MarketInfo(_Symbol, MODE_TICKSIZE);
   double safePoint = (Point > 0 && MathIsValidNumber(Point)) ? Point : 0.00001;      // CRX-027: the broker's own Point must never be 0 either
   double tk;
   if(tkRaw <= 0 || !MathIsValidNumber(tkRaw)) tk = safePoint;                       // invalid/zero MODE_TICKSIZE: fall back to Point, never 0
   else tk = tkRaw * safePoint;
   if(tk <= 0 || !MathIsValidNumber(tk) || tk > 1000.0 * safePoint) tk = safePoint;  // sanity bound: a tick size can't be absurdly larger than Point
   g_tick = (InpTickOverride > 0 && MathIsValidNumber(InpTickOverride)) ? InpTickOverride : tk;
   g_tickRaw = tkRaw; g_tickSrc = (InpTickOverride > 0) ? "Override" : "MODE_TICKSIZE*Point";
   //---- week start (MT4 has no session table: 24/7 symbols are recognised by name; set the input explicitly to override)
   if(InpWeekStart == WK_SUNDAY) g_weekSun = true;
   else if(InpWeekStart == WK_MONDAY) g_weekSun = false;
   else g_weekSun = !IsCryptoSymbol();
   IndicatorShortName("Crypex Volume Profile");
   IndicatorDigits(Digits);
   return INIT_SUCCEEDED;
  }

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

//+------------------------------------------------------------------+
//| Data loading                                                     |
//+------------------------------------------------------------------+
bool LoadChart()
  {
   int want = MathMin(InpMaxBars, Bars);
   if(want < 5) { g_status = "History not ready"; g_retry = true; return false; }
   N = want;
   ArrayResize(cTm, N); ArrayResize(cHi, N); ArrayResize(cLo, N); ArrayResize(cOp, N); ArrayResize(cCl, N); ArrayResize(cVolR, N); ArrayResize(cVolT, N);
   for(int i = 0; i < N; i++)
     {
      int sh = N - 1 - i;                      // chronological index -> MT4 shift
      cTm[i] = Time[sh]; cHi[i] = High[sh]; cLo[i] = Low[sh]; cOp[i] = Open[sh]; cCl[i] = Close[sh];
      cVolR[i] = 0.0;                          // MT4 has no real volume
      cVolT[i] = (double)Volume[sh];
     }
   return true;
  }

// lower-timeframe candles in [from, to)
void LoadLtf(const datetime from, const datetime to)
  {
   lN = 0;
   if(!g_useLtf) return;
   int total = iBars(_Symbol, g_ltf);
   if(total <= 0) { g_status = "No intrabar data: using chart bars"; g_retry = true; return; }
   int sFrom = iBarShift(_Symbol, g_ltf, from, false);
   int sTo   = iBarShift(_Symbol, g_ltf, to - 1, false);
   if(sFrom < 0) sFrom = total - 1;               // history of the lower timeframe starts later than the profile: use what exists
   if(sTo < 0) sTo = 0;
   if(sFrom < sTo) return;
   int cap = sFrom - sTo + 1;
   ArrayResize(lT, cap); ArrayResize(lH, cap); ArrayResize(lL, cap); ArrayResize(lVr, cap); ArrayResize(lVt, cap); ArrayResize(lU, cap);
   int got = 0;
   for(int sh = sFrom; sh >= sTo; sh--)
     {
      datetime t = iTime(_Symbol, g_ltf, sh);
      if(t < from) continue;
      if(t >= to) break;
      lT[got] = t; lH[got] = iHigh(_Symbol, g_ltf, sh); lL[got] = iLow(_Symbol, g_ltf, sh);
      lVr[got] = 0.0; lVt[got] = (double)iVolume(_Symbol, g_ltf, sh);
      lU[got] = iClose(_Symbol, g_ltf, sh) >= iOpen(_Symbol, g_ltf, sh);
      got++;
     }
   lN = got;
   if(lN == 0) { g_status = "No intrabar data: using chart bars"; g_retry = true; }
  }

// 1 = real volume, 0 = tick volume for the profile made of chart bars a..b
int ChooseVolType(const int a, const int b)
  {
   return 0;                                                       // MT4: tick volume only
  }

double ChartVol(const int i, const int vt)
  {
   return vt == 1 ? cVolR[i] : cVolT[i];
  }

//+------------------------------------------------------------------+
//| Profile maths (same algorithm as the TradingView version)         |
//+------------------------------------------------------------------+
// Spreads one candle's volume over the price rows by overlap with its High-Low range.
// Every candle is placed inside the grid; if a candle lies partly or fully outside it, its volume is moved to the nearest edge row
// (counted in g_clamped) so the total volume is always conserved.
void AddVol(const int k, const double h, const double l, const double v, const bool isUp)
  {
   int rowN = g_p[k].rowN;
   if(v <= 0 || h < l || rowN <= 0) return;
   double rs = g_p[k].rs, off = g_p[k].off;
   int base = g_p[k].base, s0 = g_p[k].rowStart;
   double posL = (l - off) / rs, posH = (h - off) / rs;
   int i0 = (int)MathFloor(posL + 1e-9) - base;
   int i1 = (int)MathFloor(posH + 1e-9) - base;
   if(i1 > i0 && (posH - (i1 + base)) < 1e-9) i1--;               // High exactly on a row boundary: the row above gets nothing
   bool whole = false;
   if(i1 < 0)               { i0 = 0; i1 = 0; g_clamped++; g_p[k].clamped++; whole = true; }
   else if(i0 > rowN - 1)   { i0 = rowN - 1; i1 = rowN - 1; g_clamped++; g_p[k].clamped++; whole = true; }
   else
     {
      if(i0 < 0)        { i0 = 0; g_clamped++; g_p[k].clamped++; }
      if(i1 > rowN - 1) { i1 = rowN - 1; g_clamped++; g_p[k].clamped++; }
     }
   if(i0 > i1) i0 = i1;
   double rng = h - l;
   if(whole || rng <= 0 || i0 == i1)
     {
      if(isUp) g_up[s0 + i0] += v; else g_dn[s0 + i0] += v;
     }
   else
     {
      double sum = 0;
      for(int i = i0; i <= i1; i++)
        {
         double rl = off + (base + i) * rs;
         double ov = MathMin(h, rl + rs) - MathMax(l, rl);
         if(ov > 0) sum += ov;
        }
      if(sum <= 0)
        {
         if(isUp) g_up[s0 + i0] += v; else g_dn[s0 + i0] += v;
        }
      else
         for(int i = i0; i <= i1; i++)
           {
            double rl = off + (base + i) * rs;
            double ov = MathMin(h, rl + rs) - MathMax(l, rl);
            if(ov <= 0) continue;
            double share = v * ov / sum;                          // shares add up to v exactly
            if(isUp) g_up[s0 + i] += share; else g_dn[s0 + i] += share;
           }
     }
   g_p[k].total += v;
   if(isUp) g_p[k].upTotal += v;
  }

// POC + Value Area for profile k. Returns the POC row index (-1 when empty) and fills lo / hi (row indexes).
// POC: highest volume; equal volumes (within 1e-12 of the profile total) -> the row closest to the profile centre, then the lower one.
// InpVaMethod: VA_TV (documented TradingView rule incl. its stop rule), VA_TARGET1 (1 row, until the target is reached), VA_PAIR2 (2 rows).
int CalcVA(const int k, int &lo, int &hi)
  {
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   int pocI = -1;
   lo = 0; hi = 0;
   if(n <= 0 || g_p[k].total <= 0) return -1;
   double eps = g_p[k].total * 1e-12;
   double best = -1.0, mid = (n - 1) / 2.0;
   for(int i = 0; i < n; i++)
     {
      double t = g_up[s0 + i] + g_dn[s0 + i];
      if(t > best + eps) { best = t; pocI = i; }
      else if(MathAbs(t - best) <= eps && MathAbs(i - mid) < MathAbs(pocI - mid)) { best = MathMax(best, t); pocI = i; }
     }
   double target = g_p[k].total * vVaPct / 100.0;
   lo = pocI; hi = pocI;
   if(InpVaMethod == VA_TV)
     {
      double remaining = target - best;
      while(remaining > eps && (lo > 0 || hi < n - 1))
        {
         bool hasA = hi < n - 1, hasB = lo > 0;
         double aV = hasA ? g_up[s0 + hi + 1] + g_dn[s0 + hi + 1] : -1.0;
         double bV = hasB ? g_up[s0 + lo - 1] + g_dn[s0 + lo - 1] : -1.0;
         bool takeAbove;
         if(!hasB) takeAbove = true;
         else if(!hasA) takeAbove = false;
         else if(aV > bV + eps) takeAbove = true;
         else if(bV > aV + eps) takeAbove = false;
         else takeAbove = (hi + 1 - pocI) <= (pocI - (lo - 1));       // tie: the row closer to the POC, then the upper one
         double v = takeAbove ? aV : bV;
         if(v > remaining + eps) break;                               // documented stop rule: the row would exceed the remaining target
         remaining -= v;
         if(takeAbove) hi++; else lo--;
        }
      return pocI;
     }
   double acc = best;
   int steps = (InpVaMethod == VA_PAIR2) ? 2 : 1;
   while(acc + eps < target && (lo > 0 || hi < n - 1))
     {
      double aV = 0, bV = 0;
      int aN = 0, bN = 0;
      for(int s = 1; s <= steps; s++)
        {
         if(hi + s <= n - 1) { aV += g_up[s0 + hi + s] + g_dn[s0 + hi + s]; aN++; }
         if(lo - s >= 0)     { bV += g_up[s0 + lo - s] + g_dn[s0 + lo - s]; bN++; }
        }
      bool takeAbove;
      if(aN == 0) takeAbove = false;
      else if(bN == 0) takeAbove = true;
      else if(aV > bV + eps) takeAbove = true;
      else if(bV > aV + eps) takeAbove = false;
      else takeAbove = (hi + 1 - pocI) <= (pocI - (lo - 1));
      if(takeAbove)
        {
         for(int s = 1; s <= aN; s++) acc += g_up[s0 + hi + s] + g_dn[s0 + hi + s];
         hi += aN;
        }
      else
        {
         for(int s = 1; s <= bN; s++) acc += g_up[s0 + lo - s] + g_dn[s0 + lo - s];
         lo -= bN;
        }
     }
   return pocI;
  }

void SetLevels(const int k)
  {
   int lo, hi;
   int pi = CalcVA(k, lo, hi);
   g_p[k].pocI = pi; g_p[k].vaLo = lo; g_p[k].vaHi = hi;
   g_p[k].vaCov = 0;
   if(pi < 0) { g_p[k].poc = EMPTY_VALUE; g_p[k].vah = EMPTY_VALUE; g_p[k].val = EMPTY_VALUE; return; }
   g_p[k].poc = g_p[k].off + (g_p[k].base + pi + 0.5) * g_p[k].rs;
   g_p[k].vah = g_p[k].off + (g_p[k].base + hi + 1) * g_p[k].rs;
   g_p[k].val = g_p[k].off + (g_p[k].base + lo) * g_p[k].rs;
   double vs = 0;
   for(int i = lo; i <= hi; i++) vs += g_up[g_p[k].rowStart + i] + g_dn[g_p[k].rowStart + i];
   g_p[k].vaCov = g_p[k].total > 0 ? vs / g_p[k].total * 100.0 : 0.0;
  }

// HVN = local maxima of the 1-2-1 smoothed profile; LVN = local minima between two HVNs
void FindNodes(const int k, int &hv[], int &lv[])
  {
   ArrayResize(hv, 0); ArrayResize(lv, 0);
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   if(n < 5) return;
   int nodeWin = MathMax(2, (int)MathRound(n * vNodeWinPct / 100.0));
   double sm[];
   ArrayResize(sm, n);
   double smMax = 0;
   for(int i = 0; i < n; i++)
     {
      double b = g_up[s0 + i] + g_dn[s0 + i];
      double a = i > 0 ? g_up[s0 + i - 1] + g_dn[s0 + i - 1] : b;
      double c = i < n - 1 ? g_up[s0 + i + 1] + g_dn[s0 + i + 1] : b;
      sm[i] = (a + 2 * b + c) / 4.0;
      if(sm[i] > smMax) smMax = sm[i];
     }
   if(smMax <= 0) return;
   int raw[];
   ArrayResize(raw, 0);
   for(int i = 0; i < n; i++)
     {
      double s = sm[i];
      bool isMax = true, isMin = true;
      int j0 = MathMax(0, i - nodeWin), j1 = MathMin(n - 1, i + nodeWin);
      for(int j = j0; j <= j1; j++)
        {
         if(j == i) continue;
         double v = sm[j];
         if(v > s || (v == s && j < i)) isMax = false;
         if(v < s || (v == s && j < i)) isMin = false;
        }
      if(isMax && s >= smMax * vHvnPct / 100.0) { int m = ArraySize(hv); ArrayResize(hv, m + 1); hv[m] = i; }
      if(isMin && s <= smMax * vLvnPct / 100.0 && i >= nodeWin && i <= n - 1 - nodeWin) { int m = ArraySize(raw); ArrayResize(raw, m + 1); raw[m] = i; }
     }
   int nh = ArraySize(hv);
   if(nh >= 2)
      for(int q = 0; q < ArraySize(raw); q++)
         if(raw[q] > hv[0] && raw[q] < hv[nh - 1]) { int m = ArraySize(lv); ArrayResize(lv, m + 1); lv[m] = raw[q]; }
  }

//+------------------------------------------------------------------+
//| Advanced analytics (nodes, shape, shifts, relative volume, ...)  |
//+------------------------------------------------------------------+
struct SNode
  {
   int    idx;
   double h;
   int    zl, zh;
  };

void SmoothRows(const int k, double &sm[])
  {
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   int s = MathMax(1, (int)MathFloor(n * vNodeSmoothPct / 100.0 + 0.5));
   ArrayResize(sm, n);
   for(int i = 0; i < n; i++)
     {
      double num = 0, den = 0;
      for(int d = -s; d <= s; d++)
        {
         int j = i + d;
         if(j < 0 || j >= n) continue;
         double w = s + 1 - MathAbs(d);
         num += w * (g_up[s0 + j] + g_dn[s0 + j]);
         den += w;
        }
      sm[i] = num / den;
     }
  }

double PeakProminence(const double &sm[], const int n, const int p, const double eps)
  {
   double h = sm[p], lmin = h, rmin = h;
   for(int L = p - 1; L >= 0 && sm[L] <= h + eps; L--) lmin = MathMin(lmin, sm[L]);
   for(int R = p + 1; R < n && sm[R] <= h + eps; R++) rmin = MathMin(rmin, sm[R]);
   // CRX-023: a side with nothing lower than the peak (peak on the edge of the profile) does not define a col - ignore it
   bool hasL = lmin < h - eps, hasR = rmin < h - eps;
   if(hasL && hasR) return h - MathMax(lmin, rmin);
   if(hasL) return h - lmin;
   if(hasR) return h - rmin;
   return 0.0;
  }

// Advanced nodes: HVN = prominent peaks (plateau -> centre) with a minimum separation; LVN = valley between consecutive HVNs.
// Both get a zone (rows around the extreme that stay within InpNodeZonePct of the peak / within the lowest quarter of the valley depth).
void AdvNodes(const int k, SNode &hv[], SNode &lv[], double &sm[])
  {
   ArrayResize(hv, 0); ArrayResize(lv, 0);
   int n = g_p[k].rowN;
   if(n < 5) { ArrayResize(sm, 0); return; }
   SmoothRows(k, sm);
   double mx = 0;
   for(int i = 0; i < n; i++) mx = MathMax(mx, sm[i]);
   if(mx <= 0) return;
   double eps = mx * 1e-12;
   int peaks[];
   ArrayResize(peaks, 0);
   int i = 0;
   while(i < n)
     {
      int j = i;
      while(j + 1 < n && MathAbs(sm[j + 1] - sm[i]) <= eps) j++;
      bool leftOk = (i == 0) || (sm[i - 1] < sm[i] - eps);
      bool rightOk = (j == n - 1) || (sm[j + 1] < sm[i] - eps);
      if(leftOk && rightOk && !(i == 0 && j == n - 1)) { int m = ArraySize(peaks); ArrayResize(peaks, m + 1); peaks[m] = (i + j) / 2; }
      i = j + 1;
     }
   int cand[];
   ArrayResize(cand, 0);
   for(int q = 0; q < ArraySize(peaks); q++)
     {
      int p = peaks[q];
      if(PeakProminence(sm, n, p, eps) >= vNodeProm / 100.0 * mx - eps && sm[p] >= vHvnPct / 100.0 * mx - eps)
        { int m = ArraySize(cand); ArrayResize(cand, m + 1); cand[m] = p; }
     }
   // strongest first (ties: lower row), keep a peak only if it is far enough from every accepted one
   int nc = ArraySize(cand);
   for(int a = 1; a < nc; a++)
     {
      int v = cand[a], b = a - 1;
      while(b >= 0 && (sm[cand[b]] < sm[v] || (sm[cand[b]] == sm[v] && cand[b] > v))) { cand[b + 1] = cand[b]; b--; }
      cand[b + 1] = v;
     }
   int sep = MathMax(1, (int)MathFloor(n * vNodeSep / 100.0 + 0.5));
   int acc[];
   ArrayResize(acc, 0);
   for(int q = 0; q < nc; q++)
     {
      bool ok = true;
      for(int r = 0; r < ArraySize(acc); r++) if(MathAbs(cand[q] - acc[r]) < sep) { ok = false; break; }
      if(ok) { int m = ArraySize(acc); ArrayResize(acc, m + 1); acc[m] = cand[q]; }
     }
   int na = ArraySize(acc);
   for(int a = 1; a < na; a++) { int v = acc[a], b = a - 1; while(b >= 0 && acc[b] > v) { acc[b + 1] = acc[b]; b--; } acc[b + 1] = v; }
   ArrayResize(hv, na);
   for(int q = 0; q < na; q++)
     {
      int p = acc[q];
      double thr = vNodeZonePct / 100.0 * sm[p];
      int zl = p, zh = p;
      while(zl - 1 >= 0 && sm[zl - 1] >= thr) zl--;
      while(zh + 1 < n && sm[zh + 1] >= thr) zh++;
      hv[q].idx = p; hv[q].h = sm[p]; hv[q].zl = zl; hv[q].zh = zh;
     }
   // CRX-023: neighbouring HVN zones must not overlap - the border is the lowest row between the two peaks
   for(int q = 0; q + 1 < na; q++)
     {
      int a = acc[q], b = acc[q + 1], vb = a;
      for(int x = a; x <= b; x++) if(sm[x] < sm[vb]) vb = x;
      if(hv[q].zh > vb) hv[q].zh = vb;
      if(hv[q + 1].zl <= vb) hv[q + 1].zl = MathMin(vb + 1, hv[q + 1].idx);
      if(hv[q].zh >= hv[q + 1].zl) hv[q].zh = hv[q + 1].zl - 1;
     }
   for(int q = 0; q + 1 < na; q++)
     {
      int a = acc[q], b = acc[q + 1];
      double vmin = sm[a];
      for(int x = a; x <= b; x++) vmin = MathMin(vmin, sm[x]);
      int first = a, last = b;
      for(int x = a; x <= b; x++) if(sm[x] == vmin) { first = x; break; }
      for(int x = b; x >= a; x--) if(sm[x] == vmin) { last = x; break; }
      int v = (first + last) / 2;
      double pk = MathMin(sm[a], sm[b]);
      if(vmin <= vLvnPct / 100.0 * mx + eps && pk - vmin >= vNodeProm / 100.0 * mx - eps)
        {
         double lim = vmin + 0.25 * (pk - vmin);
         int zl = v, zh = v;
         while(zl - 1 > a && sm[zl - 1] <= lim) zl--;
         while(zh + 1 < b && sm[zh + 1] <= lim) zh++;
         int m = ArraySize(lv);
         ArrayResize(lv, m + 1);
         lv[m].idx = v; lv[m].h = vmin; lv[m].zl = zl; lv[m].zh = zh;
        }
     }
  }

double RowSkew(const int k)
  {
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   double tot = 0, m = 0;
   for(int i = 0; i < n; i++) { double t = g_up[s0 + i] + g_dn[s0 + i]; tot += t; m += (i + 0.5) * t; }
   if(tot <= 0) return 0;
   m /= tot;
   double var = 0, c3 = 0;
   for(int i = 0; i < n; i++) { double t = g_up[s0 + i] + g_dn[s0 + i]; double d = (i + 0.5) - m; var += t * d * d; c3 += t * d * d * d; }
   var /= tot;
   if(var <= 1e-18) return 0;
   return c3 / tot / MathPow(MathSqrt(var), 3);
  }

// Heuristic only: D-shape / P-shape / b-shape / double distribution, otherwise Unclassified (never a market-structure claim).
string ShapeOf(const int k)
  {
   int n = g_p[k].rowN;
   if(n < 15 || g_p[k].total <= 0) return "Unclassified (thin)";
   SNode hv[], lv[];
   double sm[];
   AdvNodes(k, hv, lv, sm);
   int K = ArraySize(hv);
   if(K >= 2)
     {
      int b1 = 0, b2 = -1;                                          // the two highest peaks (ties: lower row first)
      for(int q = 1; q < K; q++) if(hv[q].h > hv[b1].h) b1 = q;
      for(int q = 0; q < K; q++) if(q != b1 && (b2 < 0 || hv[q].h > hv[b2].h)) b2 = q;
      int pa = MathMin(hv[b1].idx, hv[b2].idx), pb = MathMax(hv[b1].idx, hv[b2].idx);
      double h1 = (hv[b1].idx == pa) ? hv[b1].h : hv[b2].h, h2 = (hv[b1].idx == pa) ? hv[b2].h : hv[b1].h;
      double valley = sm[pa];
      for(int x = pa; x <= pb; x++) valley = MathMin(valley, sm[x]);
      if((pb - pa) / (double)n >= 0.25 && valley / MathMin(h1, h2) <= 0.6) return "Double distribution";
     }
   double pp = (g_p[k].pocI + 0.5) / n, sk = RowSkew(k);
   if(pp >= 0.62 && sk <= -0.3) return "P-shape";
   if(pp <= 0.38 && sk >= 0.3) return "b-shape";
   if(pp > 0.38 && pp < 0.62 && MathAbs(sk) < 0.3 && K <= 2) return "D-shape";
   return "Unclassified";
  }

string VaShiftText(const double cVal, const double cVah, const double pVal, const double pVah)
  {
   double e = 1e-9;
   if(cVal > pVah + e) return "Higher value";
   if(cVah < pVal - e) return "Lower value";
   if(cVal >= pVal - e && cVah <= pVah + e) return "Inside previous VA";
   if(cVal < pVal - e && cVah > pVah + e) return "Outside previous VA";
   return (cVal + cVah) > (pVal + pVah) ? "Overlapping higher" : "Overlapping lower";
  }

string MigrationState(const double d, const double rs)
  {
   if(MathAbs(d) <= vMigTol * rs + 1e-12) return "Stable";
   return d > 0 ? "Migrating up" : "Migrating down";
  }

// relative volume of profile k: cumulative chart-bar volume now / mean of the previous InpRvolN profiles at the same elapsed bar
// CRX-011: the cumulative series holds ACCEPTED profile volume (intrabars after the session filter, or the chart bar), and the current
// profile is compared with each earlier profile at the SAME ELAPSED TIME since that profile's own first candle - not at the same candle
// ordinal. Earlier profiles that are invalid, of another volume type (real vs tick) or too short to reach that elapsed time are left out.
double RvolOf(const int k, int &used)
  {
   used = 0;
   int kk = g_p[k].cumN;
   if(kk <= 0) return -1;
   double cur = g_cum[g_p[k].cumStart + kk - 1], sum = 0;
   long elapsed = (long)(cTm[g_p[k].i0 + kk - 1] - cTm[g_p[k].i0]);
   for(int q = k - 1; q >= 0 && q >= k - vRvolN; q--)
     {
      if(g_p[q].kind != 0 || !g_p[q].valid || g_p[q].vtype != g_p[k].vtype || g_p[q].cumN <= 0) continue;
      if((long)(cTm[g_p[q].i0 + g_p[q].cumN - 1] - cTm[g_p[q].i0]) < elapsed) continue;      // incomplete / shorter period
      int jj = -1;
      for(int j = 0; j < g_p[q].cumN; j++)
        {
         if((long)(cTm[g_p[q].i0 + j] - cTm[g_p[q].i0]) <= elapsed) jj = j; else break;
        }
      if(jj < 0) continue;
      sum += g_cum[g_p[q].cumStart + jj];
      used++;
     }
   if(used < 2) return -1;
   double a = sum / used;
   return a > 0 ? cur / a : -1;
  }

// clusters of consecutive sorted levels whose neighbour gap <= tol; keeps clusters with at least minc levels
int ClusterLevels(const double &pr[], const string &nm[], const string &sk[], const int cnt, const double tol, const int minc, double &zlo[], double &zhi[], int &zn[], string &zs[])
  {
   // CRX-022: a zone is a window of at most `tol` price width (no chaining), and it counts DISTINCT sources
   // (profile / composite / anchored) - POC+VAH+VAL of one profile are one source, not three confirmations
   int idx[];
   ArrayResize(idx, cnt);
   for(int i = 0; i < cnt; i++) idx[i] = i;
   for(int a = 1; a < cnt; a++) { int v = idx[a], b = a - 1; while(b >= 0 && pr[idx[b]] > pr[v]) { idx[b + 1] = idx[b]; b--; } idx[b + 1] = v; }
   ArrayResize(zlo, 0); ArrayResize(zhi, 0); ArrayResize(zn, 0); ArrayResize(zs, 0);
   int start = 0;
   while(start < cnt)
     {
      int end = start;
      while(end + 1 < cnt && pr[idx[end + 1]] - pr[idx[start]] <= tol + 1e-12) end++;
      string seen = "|";
      int dist = 0;
      string lab = "";
      for(int x = start; x <= end; x++)
        {
         string key = "|" + sk[idx[x]] + "|";
         if(StringFind(seen, key) >= 0) continue;
         seen += sk[idx[x]] + "|";
         dist++;
         if(dist <= 4) lab += (dist > 1 ? "+" : "") + nm[idx[x]];
        }
      if(dist >= minc)
        {
         int m = ArraySize(zlo);
         ArrayResize(zlo, m + 1); ArrayResize(zhi, m + 1); ArrayResize(zn, m + 1); ArrayResize(zs, m + 1);
         zlo[m] = pr[idx[start]]; zhi[m] = pr[idx[end]]; zn[m] = dist; zs[m] = lab;
         start = end + 1;
        }
      else start++;
     }
   return ArraySize(zlo);
  }

//+------------------------------------------------------------------+
//| Composite: resample finished profiles onto a common grid          |
//+------------------------------------------------------------------+
// Every source row keeps its volume spread uniformly over its own price span; the composite rows take the overlapping share.
// Volume is conserved exactly (shares add up to 1), so nothing is counted twice.
int MakeComposite(const int &src[], const int ns, const int rowsWanted)
  {
   if(ns <= 0) return -1;
   double lo = 1e300, hi = -1e300;
   for(int q = 0; q < ns; q++)
     {
      int p = src[q];
      lo = MathMin(lo, g_p[p].off + g_p[p].base * g_p[p].rs);
      hi = MathMax(hi, g_p[p].off + (g_p[p].base + g_p[p].rowN) * g_p[p].rs);
     }
   double rs = MathMax(g_tick, MathRound((hi - lo) / MathMax(1, rowsWanted) / g_tick) * g_tick);   // CRX-002/CRX-014: tick-aligned
   int n = (int)MathMax(1.0, MathCeil((hi - lo) / rs - 1e-9));
   int k = ArraySize(g_p);
   ArrayResize(g_p, k + 1);
   int s0 = ArraySize(g_up);
   ArrayResize(g_up, s0 + n); ArrayResize(g_dn, s0 + n);
   for(int x = 0; x < n; x++) { g_up[s0 + x] = 0.0; g_dn[s0 + x] = 0.0; }
   double tot = 0, upT = 0, mnLo = 1e300, mxHi = -1e300;
   datetime t0 = g_p[src[0]].t0, t1 = g_p[src[0]].t1;
   int i0 = g_p[src[0]].i0, i1 = g_p[src[0]].i1, vt = 1;
   for(int q = 0; q < ns; q++)
     {
      int p = src[q];
      for(int i = 0; i < g_p[p].rowN; i++)
        {
         double vu = g_up[g_p[p].rowStart + i], vd = g_dn[g_p[p].rowStart + i];
         if(vu <= 0 && vd <= 0) continue;
         double b = g_p[p].off + (g_p[p].base + i) * g_p[p].rs, tp = b + g_p[p].rs;
         int j0 = (int)MathMax(0.0, MathFloor((b - lo) / rs + 1e-9));
         int j1 = (int)MathMin((double)(n - 1), MathFloor((tp - lo) / rs - 1e-9));
         if(j1 < j0) j1 = j0;
         double S = 0;
         for(int j = j0; j <= j1; j++) S += MathMax(0.0, MathMin(tp, lo + (j + 1) * rs) - MathMax(b, lo + j * rs));
         for(int j = j0; j <= j1; j++)
           {
            double ov = MathMax(0.0, MathMin(tp, lo + (j + 1) * rs) - MathMax(b, lo + j * rs));
            double f = S > 0 ? ov / S : (j == j0 ? 1.0 : 0.0);
            g_up[s0 + j] += vu * f; g_dn[s0 + j] += vd * f;
           }
        }
      tot += g_p[p].total; upT += g_p[p].upTotal;
      mnLo = MathMin(mnLo, g_p[p].lo); mxHi = MathMax(mxHi, g_p[p].hi);
      if(g_p[p].t0 < t0) { t0 = g_p[p].t0; i0 = g_p[p].i0; }
      if(g_p[p].t1 > t1) { t1 = g_p[p].t1; i1 = g_p[p].i1; }
      if(g_p[p].vtype == 0) vt = 0;
     }
   g_p[k].i0 = i0; g_p[k].i1 = i1; g_p[k].t0 = t0; g_p[k].t1 = t1;
   g_p[k].rs = rs; g_p[k].off = lo; g_p[k].base = 0; g_p[k].rowStart = s0; g_p[k].rowN = n;
   g_p[k].total = tot; g_p[k].upTotal = upT; g_p[k].hi = mxHi; g_p[k].lo = mnLo;
   g_p[k].poc = EMPTY_VALUE; g_p[k].vah = EMPTY_VALUE; g_p[k].val = EMPTY_VALUE;
   g_p[k].vaCov = 0; g_p[k].vtype = vt; g_p[k].valid = true; g_p[k].naked = false; g_p[k].prov = false; g_p[k].touchBar = -1;
   g_p[k].kind = 1; g_p[k].slot = 0; g_p[k].cumStart = 0; g_p[k].cumN = 0; g_p[k].ibH = EMPTY_VALUE; g_p[k].ibL = EMPTY_VALUE; g_p[k].ibEst = false;
   g_p[k].qIntra = 0; g_p[k].qFb = 0; g_p[k].qSkip = 0; g_p[k].qExp = 0; g_p[k].qGot = 0; g_p[k].qSparse = 0; g_p[k].qPartial = 0; g_p[k].qTypeMis = 0; g_p[k].qVolIn = 0; g_p[k].qVolChart = 0;
   for(int q = 0; q < ns; q++)
     {
      int p = src[q];
      g_p[k].qIntra += g_p[p].qIntra; g_p[k].qFb += g_p[p].qFb; g_p[k].qSkip += g_p[p].qSkip; g_p[k].qExp += g_p[p].qExp; g_p[k].qGot += g_p[p].qGot;
      g_p[k].qSparse += g_p[p].qSparse; g_p[k].qPartial += g_p[p].qPartial; g_p[k].qTypeMis += g_p[p].qTypeMis; g_p[k].qVolIn += g_p[p].qVolIn; g_p[k].qVolChart += g_p[p].qVolChart;
     }
   g_p[k].bPrecise = 0; g_p[k].bEstimated = 0; g_p[k].bExcluded = 0; g_p[k].clamped = 0;
   SetLevels(k);
   double rowSum = 0;
   for(int x = 0; x < n; x++) rowSum += g_up[s0 + x] + g_dn[s0 + x];
   if(MathAbs(rowSum - tot) > 1e-9 * MathMax(1.0, tot)) g_p[k].valid = false;
   return k;
  }

// first CLOSED chart bar after the profile that revisits its POC (wick touch, or a close through the level); -1 = still naked.
// The forming bar (N-1) is never used here, so the confirmed state cannot flip back after a reload.
int FirstTouch(const int k)
  {
   double poc = g_p[k].poc;
   if(poc == EMPTY_VALUE) return -1;
   double side = (cCl[g_p[k].i1] - poc) >= 0 ? 1.0 : -1.0;
   for(int i = g_p[k].i1 + 1; i < N - 1; i++)
     {
      if(InpNakedTouch == NT_WICK) { if(cLo[i] <= poc && cHi[i] >= poc) return i; }
      else if((cCl[i] - poc) * side <= 0) return i;
     }
   return -1;
  }

//+------------------------------------------------------------------+
//| Build all profiles                                               |
//+------------------------------------------------------------------+
int LtfLB(const datetime t)                              // first lower-timeframe candle with time >= t
  {
   int lo = 0, hi = lN;
   while(lo < hi) { int mid = (lo + hi) / 2; if(lT[mid] < t) lo = mid + 1; else hi = mid; }
   return lo;
  }

int ChartLB(const datetime t)                            // first chart bar with time >= t
  {
   int lo = 0, hi = N;
   while(lo < hi) { int mid = (lo + hi) / 2; if(cTm[mid] < t) lo = mid + 1; else hi = mid; }
   return lo;
  }

// periods (chronological) of the chart bars up to 'last', newest 'want' of them.
// CRX-001: bb[k] is the chart bar just BEFORE block k (index p0[k]-1) when that bar's own OPEN time is outside the
// period but its LAST instant (open + chartSec - 1) is already inside it — i.e. a candle that straddles the boundary
// and would otherwise be silently dropped in full (its in-period tail is real data, not noise).
double LocalMinuteOfDay(const datetime t, const int filt);

// CRX-028: period id of candle i. Normally the id of its open time. A session SHORTER than the chart bar can lie entirely
// inside one candle (e.g. H4 08:00-12:00 holding session 08:30-10:00): then no candle opens inside the session and the
// session was lost. In that case the id of the session start instant inside the candle is returned, so the candle becomes
// a (boundary) candle of that session. ff: 0 = primary (only in session mode), 1..4 = session slot.
long CandIdS(const int i, const int ff)
  {
   long id = IdOf(cTm[i], ff);
   if(id >= 0 || g_chartSec <= 60) return id;
   if(ff == 0 && g_effPeriod != VP_SESSION) return id;
   double sSm = (ff == 0) ? (double)g_sessS : (double)g_slotS[ff - 1];
   double dm = sSm - LocalMinuteOfDay(cTm[i], ff);
   if(dm < 0) dm += 1440.0;
   if(dm * 60.0 >= g_chartSec) return id;                                      // the session does not start inside this candle
   long pid2 = IdOf(cTm[i] + (datetime)MathRound(dm * 60.0), ff);
   if(pid2 < 0) return id;
   if(i + 1 < N && cTm[i + 1] == cTm[i] + (datetime)g_chartSec && IdOf(cTm[i + 1], ff) == pid2) return id;   // continues into the next candle: that one owns it (boundary-before)
   return pid2;
  }

int SplitPeriods(const int filt, const int want, const int last, int &p0[], int &p1[], int &bb[])
  {
   ArrayResize(p0, 0); ArrayResize(p1, 0); ArrayResize(bb, 0);
   int ff = (filt == 100) ? 0 : filt;                     // id function: filt 100 = primary session mode (still PeriodId, ff=0)
   int i = last;
   while(i >= 0 && ArraySize(p0) < want)
     {
      long id = CandIdS(i, ff);
      if(id < 0) { i--; continue; }
      int e = i;
      while(i - 1 >= 0 && IdOf(cTm[i - 1], ff) == id) i--;
      int m = ArraySize(p0);
      ArrayResize(p0, m + 1); ArrayResize(p1, m + 1); ArrayResize(bb, m + 1);
      p0[m] = i; p1[m] = e;
      bb[m] = -1;
      if(filt != 0 && i - 1 >= 0 && IdOf(cTm[i - 1] + (datetime)g_chartSec - 1, ff) == id) bb[m] = i - 1;
      i--;
     }
   int n = ArraySize(p0);
   for(int a = 0; a < n / 2; a++)
     {
      int t0 = p0[a], t1 = p1[a], tb = bb[a];
      p0[a] = p0[n - 1 - a]; p1[a] = p1[n - 1 - a]; bb[a] = bb[n - 1 - a];
      p0[n - 1 - a] = t0; p1[n - 1 - a] = t1; bb[n - 1 - a] = tb;
     }
   return n;
  }

// local minute-of-day of a server timestamp, under the time basis that IdOf(.., filt) itself uses (primary InpTz, or the slot's own zone)
double LocalMinuteOfDay(const datetime t, const int filt)
  {
   datetime tl = (filt >= 1 && filt <= 4) ? ToLocalTz(t, SlotTz(filt - 1)) : ToLocal(t);
   MqlDateTime d;
   TimeToStruct(tl, d);
   return d.hour * 60 + d.min + d.sec / 60.0;
  }

double OvRange(const double m0, const double m1, const double lo, const double hi)
  {
   double x = MathMin(m1, hi) - MathMax(m0, lo);
   return x > 0 ? x : 0.0;
  }

// CRX-001: share of chart bar [t, t+chartSec) that falls inside the session [sS,sE) (minutes of day, sS>sE = overnight wrap).
// Assumes no DST jump strictly inside this one bar (documented approximation, see CRX-013 / Known Limitations).
double OverlapFraction(const datetime t, const int filt, const double sS, const double sE)
  {
   if(filt == 200) return MathMin(1.0, MathMax(0.0, (double)((long)t + g_chartSec - (long)InpAncTime) / g_chartSec));
   double m0 = LocalMinuteOfDay(t, filt);
   double m1 = m0 + g_chartSec / 60.0;
   double covered;
   if(sS < sE) covered = OvRange(m0, m1, sS, sE);
   else covered = MathMax(OvRange(m0, m1, sS, 1440) + OvRange(m0, m1, 0, sE), MathMax(OvRange(m0, m1, sS - 1440, sE), OvRange(m0, m1, sS, sE + 1440)));
   double frac = covered / (g_chartSec / 60.0);
   return MathMin(1.0, MathMax(0.0, frac));
  }

// true when candle i is only PARTIALLY inside the period 'pid' (its open or its last instant falls outside it) — only
// meaningful when useFilter; such a candle's full H/L/volume must never be added blindly (CRX-001).
bool IsBoundaryCandle(const int i, const int filt, const long pid)
  {
   bool openIn = IdOf(cTm[i], filt) == pid;
   bool closeIn = IdOf(cTm[i] + (datetime)g_chartSec - 1, filt) == pid;
   return !(openIn && closeIn);
  }

// builds ONE profile from chart bars a..b and appends it. kind: 0 primary, 2 anchored, 3 session slot.
// filt: 0 = no filter, 100 = primary session mode (id == pid), 1..4 = session slot (id == pid).
// boundaryBefore >= 0: an extra candle just before 'a' whose tail overlaps the period (see SplitPeriods / CRX-001).
// sS/sE: session minutes-of-day, only used when filt != 0 (for the boundary-candle overlap fraction).
// rowsExact > 0: grid fitted to the exact range (rows = rowsExact), otherwise the row size comes from 'est' (typical range) / rows.
int BuildOne(const int a, const int b, const int kind, const int slot, const int filt, const long pid,
             const double est, const int rowsExact, const bool dev, const int boundaryBefore = -1,
             const double sS = 0, const double sE = 0)
  {
   int k = ArraySize(g_p);
   ArrayResize(g_p, k + 1);
   int vt = ChooseVolType(a, b);
   int ff = (filt == 100) ? 0 : filt;                    // id function: primary period or slot
   bool useFilter = (filt != 0);
   bool headB = (filt == 200 && cTm[a] < InpAncTime);              // CRX-016: the anchor falls INSIDE the first candle - only its intrabars from the anchor on belong to the profile
   double hi = cHi[a], lo = cLo[a];
   if(headB && a < b) { hi = cHi[a + 1]; lo = cLo[a + 1]; }
   for(int i = headB ? a + 1 : a; i <= b; i++) { if(cHi[i] > hi) hi = cHi[i]; if(cLo[i] < lo) lo = cLo[i]; }
   if(g_useLtf)                                          // intrabar extremes too, so that every candle lies inside the grid
     {
      datetime tS = cTm[a], tE = cTm[b] + g_chartSec;
      for(int j = LtfLB(tS); j < lN && lT[j] < tE; j++)
        {
         if(useFilter && IdOf(lT[j], ff) != pid) continue;
         if(lH[j] > hi) hi = lH[j];
         if(lL[j] < lo) lo = lL[j];
        }
     }
   if(headB && InpSessBound == SESSB_ESTIMATE && InpVolSrc != SRC_STRICT) { if(cHi[a] > hi) hi = cHi[a]; if(cLo[a] < lo) lo = cLo[a]; }   // the estimate (if it is needed) uses the whole candle's range
   // CRX-001: the boundary-before candle's own H/L must never widen the grid wholesale (it is only partly in the
   // period). Use its filtered intrabars when they exist; otherwise only the Estimate policy borrows the full candle.
   int bbJ0 = 0, bbJ1 = 0;
   bool bbUseIntra = false;
   if(boundaryBefore >= 0)
     {
      bbJ0 = LtfLB(cTm[boundaryBefore]); bbJ1 = LtfLB(cTm[boundaryBefore] + g_chartSec);
      if(bbJ1 > bbJ0)
        {
         for(int j = bbJ0; j < bbJ1; j++) if(IdOf(lT[j], ff) == pid) { if(lH[j] > hi) hi = lH[j]; if(lL[j] < lo) lo = lL[j]; }
         if(InpVolSrc == SRC_STRICT) bbUseIntra = true;
         else
           {
            double bbSumL = 0;
            for(int j = bbJ0; j < bbJ1; j++) bbSumL += (vt == 1 ? lVr[j] : lVt[j]);
            double bbCv = ChartVol(boundaryBefore, vt);
            bbUseIntra = (bbCv <= 0 && bbSumL <= 0) || (bbCv > 0 && MathAbs(bbSumL - bbCv) <= bbCv * vVolTol / 100.0);
           }
         if(!bbUseIntra && InpSessBound == SESSB_ESTIMATE)              // the estimated add below will use the full candle's range too
           {
            if(cHi[boundaryBefore] > hi) hi = cHi[boundaryBefore];
            if(cLo[boundaryBefore] < lo) lo = cLo[boundaryBefore];
           }
        }
      else if(InpSessBound == SESSB_ESTIMATE)
        {
         if(cHi[boundaryBefore] > hi) hi = cHi[boundaryBefore];
         if(cLo[boundaryBefore] < lo) lo = cLo[boundaryBefore];
        }
     }
   double tick = g_tick, rs, off = 0.0;
   if(rowsExact > 0)
     {
      rs = MathMax(tick, MathRound((hi - lo) / rowsExact / tick) * tick);    // CRX-002/CRX-014: row size always a whole multiple of the tick size
      off = lo;
     }
   else
     {
      if(InpRowMode == ROWS_FIXED) rs = vRowTicks * tick;
      else rs = MathMax(tick, MathRound(est / MathMax(1, vRows) / tick) * tick);
      rs = MathMax(rs, MathCeil(est / 1500.0 / tick) * tick);       // safety cap: at most ~1500 rows from the estimate
     }
   if(rs <= 0) rs = tick;
   int base = (int)MathFloor((lo - off) / rs + 1e-9);
   int top = (int)MathCeil((hi - off) / rs - 1e-9) - 1;
   if(top < base) top = base;
   if(top - base + 1 > 2000)                             // too many rows: coarser grid BEFORE any volume is added (volume stays conserved)
     {
      rs = MathMax(tick, MathCeil((hi - lo) / 1999.0 / tick) * tick);                // CRX-002/CRX-014: tick-aligned, rounded UP so the cap really holds
      base = (int)MathFloor((lo - off) / rs + 1e-9);
      top = (int)MathCeil((hi - off) / rs - 1e-9) - 1;
      if(top < base) top = base;
      while(top - base + 1 > 2000) { rs += tick; base = (int)MathFloor((lo - off) / rs + 1e-9); top = (int)MathCeil((hi - off) / rs - 1e-9) - 1; if(top < base) top = base; }
     }
   int rowN = top - base + 1;
   int s0 = ArraySize(g_up);
   ArrayResize(g_up, s0 + rowN); ArrayResize(g_dn, s0 + rowN);
   for(int q = 0; q < rowN; q++) { g_up[s0 + q] = 0.0; g_dn[s0 + q] = 0.0; }
   int c0 = ArraySize(g_cum);
   ArrayResize(g_cum, c0 + (b - a + 1));
   g_p[k].i0 = a; g_p[k].i1 = b; g_p[k].t0 = cTm[a]; g_p[k].t1 = cTm[b];
   g_p[k].rs = rs; g_p[k].off = off; g_p[k].base = base; g_p[k].rowStart = s0; g_p[k].rowN = rowN;
   g_p[k].total = 0; g_p[k].upTotal = 0; g_p[k].hi = hi; g_p[k].lo = lo;
   g_p[k].poc = EMPTY_VALUE; g_p[k].vah = EMPTY_VALUE; g_p[k].val = EMPTY_VALUE;
   g_p[k].vaCov = 0; g_p[k].vtype = vt; g_p[k].valid = true; g_p[k].naked = true; g_p[k].prov = false; g_p[k].touchBar = -1;
   g_p[k].kind = kind; g_p[k].slot = slot; g_p[k].cumStart = c0; g_p[k].cumN = b - a + 1;
   g_p[k].ibH = EMPTY_VALUE; g_p[k].ibL = EMPTY_VALUE; g_p[k].ibEst = false;
   g_p[k].qIntra = 0; g_p[k].qFb = 0; g_p[k].qSkip = 0; g_p[k].qExp = 0; g_p[k].qGot = 0; g_p[k].qSparse = 0; g_p[k].qPartial = 0; g_p[k].qTypeMis = 0; g_p[k].qVolIn = 0; g_p[k].qVolChart = 0;
   g_p[k].bPrecise = 0; g_p[k].bEstimated = 0; g_p[k].bExcluded = 0; g_p[k].clamped = 0;
   if(kind == 0) { if(vt == 1) g_nReal++; else g_nTick++; }

   // CRX-001: the boundary-before candle (its own open time is outside the period, but its tail is in it).
   // Fully precise when intrabars exist (they are filtered to the in-period minutes, same as any candle below);
   // otherwise the chosen policy decides: silently add nothing (Exclude) or a time-prorated, clearly counted estimate.
   if(boundaryBefore >= 0)
     {
      if(bbUseIntra)
        {
         for(int j = bbJ0; j < bbJ1; j++) if(IdOf(lT[j], ff) == pid) AddVol(k, lH[j], lL[j], vt == 1 ? lVr[j] : lVt[j], lU[j]);
         g_p[k].bPrecise++;
        }
      else if(InpVolSrc == SRC_STRICT)
         g_p[k].bExcluded++;                                         // Strict: no usable intrabars for the boundary candle (none at all, or bbUseIntra stayed false) -> nothing added
      else if(InpSessBound == SESSB_ESTIMATE)
        {
         double bbFrac = OverlapFraction(cTm[boundaryBefore], filt, sS, sE);
         AddVol(k, cHi[boundaryBefore], cLo[boundaryBefore], ChartVol(boundaryBefore, vt) * bbFrac, cCl[boundaryBefore] >= cOp[boundaryBefore]);
         g_p[k].bEstimated++;
        }
      else
         g_p[k].bExcluded++;
     }

   double cum = 0;
   for(int i = a; i <= b; i++)
     {
      bool isBoundary = useFilter && IsBoundaryCandle(i, ff, pid);      // only the LAST candle of the block can be one (see SplitPeriods)
      datetime tb0 = cTm[i], tb1 = cTm[i] + g_chartSec;
      int j0 = LtfLB(tb0), j1 = LtfLB(tb1);
      double sumL = 0;
      for(int j = j0; j < j1; j++) sumL += (vt == 1 ? lVr[j] : lVt[j]);
      bool useIntra = false;
      if(g_useLtf && j1 > j0)
        {
         double cv = ChartVol(i, vt);                       // intrabars must add up to the chart bar, never both, never rescaled
         bool volMatch = (cv <= 0 && sumL <= 0) || (cv > 0 && MathAbs(sumL - cv) <= cv * vVolTol / 100.0);
         if(InpVolSrc == SRC_STRICT) useIntra = true;
         else useIntra = volMatch;
         if(!isBoundary)
           {
            // CRX-008/017: time coverage = LTF slots present / slots expected; volume coverage = intrabar volume / chart-bar volume (per profile; globals only for primary profiles).
            // A candle with missing slots whose volume still matches is treated as "minutes without trades", not as lost data.
            int expN = (int)MathMax(1, g_chartSec / MathMax(1, PeriodSeconds(g_ltf)));
            long gotN = MathMin(expN, j1 - j0);
            g_p[k].qExp += expN; g_p[k].qGot += gotN;
            if(volMatch && j1 - j0 < expN) g_p[k].qSparse++;
            if(!volMatch && InpVolSrc == SRC_STRICT) g_p[k].qPartial++;
            if(cv > 0 && sumL <= 0) g_p[k].qTypeMis++;
            if(cv > 0) { g_p[k].qVolChart += cv; g_p[k].qVolIn += MathMin(cv, sumL); }
            if(kind == 0)
              {
               g_cvExp += expN; g_cvGot += gotN;
               if(volMatch && j1 - j0 < expN) g_cvSparse++;
               if(!volMatch && InpVolSrc == SRC_STRICT) g_cvPartial++;
               if(cv > 0 && sumL <= 0) g_cvTypeMis++;
               if(cv > 0) { g_cvVolChart += cv; g_cvVolIn += MathMin(cv, sumL); }
              }
           }
        }
      double addedVol = 0.0;
      if(useIntra)
        {
         for(int j = j0; j < j1; j++)
           {
            if(useFilter && IdOf(lT[j], ff) != pid) continue;                // intrabars outside the session do not belong to it
            double vv = vt == 1 ? lVr[j] : lVt[j];
            AddVol(k, lH[j], lL[j], vv, lU[j]);
            addedVol += vv;
           }
         g_p[k].qIntra++;
         if(kind == 0) g_ltfBars++;
         if(isBoundary) g_p[k].bPrecise++;
        }
      else if(InpVolSrc == SRC_STRICT && g_useLtf)
        {
         g_p[k].qSkip++;
         if(kind == 0) g_skipBars++;
         if(isBoundary) g_p[k].bExcluded++;
        }
      else if(!isBoundary)
        {
         addedVol = ChartVol(i, vt);
         AddVol(k, cHi[i], cLo[i], addedVol, cCl[i] >= cOp[i]);
         g_p[k].qFb++;
         if(kind == 0) g_fbBars++;
        }
      else if(InpSessBound == SESSB_ESTIMATE)                             // boundary candle, no usable intrabars: prorate, never blind-add
        {
         double frac = OverlapFraction(cTm[i], filt, sS, sE);
         addedVol = ChartVol(i, vt) * frac;
         AddVol(k, cHi[i], cLo[i], addedVol, cCl[i] >= cOp[i]);
         g_p[k].bEstimated++;
        }
      else
         g_p[k].bExcluded++;                                              // Exclude policy (default): contributes nothing, counted
      cum += addedVol;
      g_cum[c0 + (i - a)] = cum;
      if(dev)
        {
         int lo2, hi2;
         int pI = CalcVA(k, lo2, hi2);
         if(pI >= 0)
           {
            BufPoc[N - 1 - i] = g_p[k].off + (g_p[k].base + pI + 0.5) * g_p[k].rs;
            BufVah[N - 1 - i] = g_p[k].off + (g_p[k].base + hi2 + 1) * g_p[k].rs;
            BufVal[N - 1 - i] = g_p[k].off + (g_p[k].base + lo2) * g_p[k].rs;
           }
        }
     }
   if(kind == 0)                                         // Initial Balance: the first vIBMin minutes counted from the REAL period start
     {
      // CRX-012: in Session mode the window starts at the session's own start time (which can fall inside a candle), not at the first chart bar;
      // the window is [ibS, ibE). Candles fully inside use their H/L; partly-covered candles use only the intrabars inside the window,
      // and are left out (IB flagged approximate) when no intrabars exist. Overnight sessions work because the start is derived per profile.
      datetime ibS = cTm[a];
      int ib0 = a;
      if(useFilter && boundaryBefore >= 0)
        {
         double m0 = LocalMinuteOfDay(cTm[boundaryBefore], filt);
         double x = MathMod(sS - m0 + 2880.0, 1440.0);
         if(x > 0 && x < g_chartSec / 60.0) { ibS = cTm[boundaryBefore] + (datetime)MathRound(x * 60.0); ib0 = boundaryBefore; }
        }
      datetime ibE = ibS + (datetime)(vIBMin * 60);
      for(int i = ib0; i <= b && cTm[i] < ibE; i++)
        {
         datetime c0t = cTm[i], c1t = cTm[i] + g_chartSec;
         double h2 = EMPTY_VALUE, l2 = EMPTY_VALUE;
         if(c0t >= ibS && c1t <= ibE) { h2 = cHi[i]; l2 = cLo[i]; }
         else
           {
            int jA = LtfLB(c0t), jB = LtfLB(c1t);
            for(int j = jA; j < jB; j++)
              {
               if(lT[j] < ibS || lT[j] >= ibE) continue;
               if(useFilter && IdOf(lT[j], ff) != pid) continue;
               if(h2 == EMPTY_VALUE || lH[j] > h2) h2 = lH[j];
               if(l2 == EMPTY_VALUE || lL[j] < l2) l2 = lL[j];
              }
            if(h2 == EMPTY_VALUE && c1t > ibS && c0t < ibE) g_p[k].ibEst = true;      // partly inside the window, nothing to measure it with
           }
         if(h2 == EMPTY_VALUE) continue;
         if(g_p[k].ibH == EMPTY_VALUE || h2 > g_p[k].ibH) g_p[k].ibH = h2;
         if(g_p[k].ibL == EMPTY_VALUE || l2 < g_p[k].ibL) g_p[k].ibL = l2;
        }
     }
   SetLevels(k);
   double rowSum = 0;
   for(int q = 0; q < rowN; q++) rowSum += g_up[s0 + q] + g_dn[s0 + q];
   // CRX-003: conservation of the SUM alone is not enough - any row the data had to be clamped into makes the
   // DISTRIBUTION untrustworthy too (its price location is wrong), so the whole profile is marked invalid.
   if(MathAbs(rowSum - g_p[k].total) > 1e-9 * MathMax(1.0, g_p[k].total) || g_p[k].upTotal > g_p[k].total + 1e-9 * MathMax(1.0, g_p[k].total) || g_p[k].clamped > 0) g_p[k].valid = false;
   if(g_strictNA) g_p[k].valid = false;                                  // CRX-009: Strict never silently falls back to chart bars
   return k;
  }

double RangeEstimate(const int a, const double fallbackRange)       // bootstrap: 3 x the average candle range before bar a
  {
   int c0 = MathMax(1, a - 20), cnt = 0;
   double e = 0;
   for(int q = c0; q <= a && q < N; q++) { e += cHi[q] - cLo[q]; cnt++; }
   return (cnt > 0 ? e / cnt : fallbackRange) * 3.0;
  }

void BuildProfiles()
  {
   g_np = 0; g_tot = 0;
   ArrayResize(g_p, 0); ArrayResize(g_up, 0); ArrayResize(g_dn, 0); ArrayResize(g_cum, 0);
   ArrayInitialize(BufPoc, EMPTY_VALUE); ArrayInitialize(BufVah, EMPTY_VALUE); ArrayInitialize(BufVal, EMPTY_VALUE);
   g_curIdx = -1; g_doneIdx = -1; g_clamped = 0; g_compMixEx = 0; g_compCut = 0; g_compN = 0; g_realEmpty = 0; g_nReal = 0; g_nTick = 0; g_compIdx = -1; g_ancIdx = -1;
   for(int s = 0; s < 4; s++) { g_msA[s] = -1; g_msB[s] = -1; g_msRun[s] = false; }
   int last = N - 2;                                     // closed bars only
   if(last < 2) return;
   int wantP = vKeepPrev + 1 + (InpNaked ? vNakedLook : 0);
   if(InpRvol) wantP = MathMax(wantP, vRvolN + 2);
   if(InpComp) wantP = MathMax(wantP, InpCompMode == COMP_LASTN ? vCompN + 1 : 40);

   //---- 1) period lists: primary, anchored start, session slots
   int pi0[], pi1[], pib[];
   ArrayResize(pi0, 0); ArrayResize(pi1, 0); ArrayResize(pib, 0);
   if(g_effPeriod == VP_ROLL)
     {
      ArrayResize(pi0, 1); ArrayResize(pi1, 1); ArrayResize(pib, 1);
      pi0[0] = MathMax(0, last - vRollN + 1); pi1[0] = last; pib[0] = -1;
     }
   else SplitPeriods(g_effPeriod == VP_SESSION ? 100 : 0, wantP, last, pi0, pi1, pib);
   int np = ArraySize(pi0);
   int ancA = -1;
   g_ancState = 0; g_ancLostSec = 0;
   if(InpAnc && InpAncTime > 0)
     {
      int c0a = ChartLB(InpAncTime);                           // first candle opening at/after the anchor
      if(c0a > last && !(last >= 0 && InpAncTime < cTm[last] + g_chartSec)) { ancA = -1; g_ancState = 5; }     // anchor lies after the newest candle
      else if(c0a == 0 && InpAncTime < cTm[0]) { ancA = 0; g_ancState = 4; }                                  // before the available history
      else if(c0a <= last && cTm[c0a] == InpAncTime) { ancA = c0a; g_ancState = 1; }                         // exactly on a candle open
      else
        {
         int cc = c0a - 1;                                      // candle that contains / precedes the anchor
         bool inside = cc >= 0 && InpAncTime < cTm[cc] + g_chartSec;
         if(!inside) { ancA = c0a; g_ancState = 2; }            // anchor in a gap between candles (market closed): next candle, nothing lost
         else if(g_useLtf) { ancA = cc; g_ancState = 6; }       // mid-candle, intrabars cut it exactly at the anchor
         else { ancA = c0a; g_ancState = 3; g_ancLostSec = (int)(cTm[cc] + g_chartSec - InpAncTime); }   // mid-candle, no intrabars: snapped forward
         if(ancA > last) { ancA = -1; g_ancState = 5; }
        }
     }
   int sp0[4][2], sp1[4][2], spb[4][2], spn[4];
   for(int s = 0; s < 4; s++)
     {
      spn[s] = 0;
      if(!SlotOn(s)) continue;
      int t0[], t1[], tb[];
      int n2 = SplitPeriods(s + 1, 2, last, t0, t1, tb);
      spn[s] = n2;
      for(int q = 0; q < n2; q++) { sp0[s][q] = t0[q]; sp1[s][q] = t1[q]; spb[s][q] = tb[q]; }
     }
   if(np == 0 && ancA < 0 && spn[0] + spn[1] + spn[2] + spn[3] == 0) return;

   //---- 2) lower-timeframe data once for the earliest start of anything that needs it
   int from = last;
   if(np > 0) from = MathMin(from, pib[0] >= 0 ? pib[0] : pi0[0]);   // CRX-001: also load the oldest profile's boundary-before candle
   if(ancA >= 0) from = MathMin(from, ancA);
   for(int s = 0; s < 4; s++) if(spn[s] > 0) from = MathMin(from, spb[s][0] >= 0 ? spb[s][0] : sp0[s][0]);
   LoadLtf(cTm[from], cTm[last] + g_chartSec);

   //---- 3) primary profiles
   int dispFrom = MathMax(0, np - (vKeepPrev + 1));
   double hist[5];
   int histN = 0;
   for(int k = 0; k < np; k++)
     {
      int a = pi0[k], b = pi1[k];
      long pid = (g_effPeriod == VP_ROLL) ? -2 : CandIdS(a, 0);
      double est = 0;
      if(g_effPeriod != VP_ROLL)
        {
         if(histN > 0) { for(int q = 0; q < histN; q++) est += hist[q]; est /= histN; }
         else { double h0 = cHi[a], l0 = cLo[a]; for(int i = a; i <= b; i++) { if(cHi[i] > h0) h0 = cHi[i]; if(cLo[i] < l0) l0 = cLo[i]; } est = RangeEstimate(a, h0 - l0); }
        }
      int idx = BuildOne(a, b, 0, 0, g_effPeriod == VP_SESSION ? 100 : 0, pid, est, (g_effPeriod == VP_ROLL || InpRowMode == ROWS_COUNT) ? vRows : 0,
                         InpShowDev && g_effPeriod != VP_ROLL && k >= dispFrom, pib[k], g_sessS, g_sessE);
      if(histN == 5) { for(int q = 0; q < 4; q++) hist[q] = hist[q + 1]; histN = 4; }
      hist[histN++] = g_p[idx].hi - g_p[idx].lo;
     }
   g_np = np;

   //---- 4) current vs finished: a profile is "current" only if the FORMING bar belongs to the same period
   if(np > 0)
     {
      if(g_effPeriod == VP_ROLL) { g_curIdx = 0; g_doneIdx = -1; }
      else
        {
         long formingId = CandIdS(N - 1, 0);
         long lastId = CandIdS(g_p[np - 1].i1, 0);
         if(formingId >= 0 && formingId == lastId) { g_curIdx = np - 1; g_doneIdx = np - 2; }
         else { g_curIdx = -1; g_doneIdx = np - 1; }
        }
     }
   //---- 5) naked POC lifecycle (first revisit by wick or by close, see InpNakedTouch)
   for(int k = 0; k < np; k++)
     {
      g_p[k].touchBar = -1; g_p[k].naked = false; g_p[k].prov = false;
      if(g_p[k].poc == EMPTY_VALUE || k == g_curIdx || g_effPeriod == VP_ROLL) continue;
      g_p[k].touchBar = FirstTouch(k);
      g_p[k].naked = g_p[k].touchBar < 0;
      g_p[k].prov = InpNakedProv && g_p[k].naked && InpNakedTouch == NT_WICK && cLo[N - 1] <= g_p[k].poc && cHi[N - 1] >= g_p[k].poc;
     }
   //---- 6) composite of the primary profiles (resampled, nothing counted twice)
   if(InpComp && g_effPeriod != VP_ROLL && np > 0)
     {
      int src[];
      ArrayResize(src, 0);
      ENUM_VP_PERIOD ck = InpCompMode == COMP_MONTH ? VP_MONTH : VP_WEEK;
      long wk = PeriodIdKind(ck, cTm[N - 1]);
      // CRX-015: a window composite only sums profiles that lie COMPLETELY inside the window; a primary period coarser than the window cannot be cut apart
      bool compBad = (InpCompMode == COMP_WEEK && g_effPeriod == VP_MONTH);
      if(compBad)
         g_status = "Composite 'Current week' needs a primary period of Week or shorter (monthly profiles cannot be split) - composite not built";
      for(int k = np - 1; k >= 0 && !compBad; k--)
        {
         if(!g_p[k].valid) continue;
         if(InpCompMode == COMP_LASTN) { if(ArraySize(src) >= vCompN) break; }
         else
           {
            bool inA = PeriodIdKind(ck, g_p[k].t0) == wk, inB = PeriodIdKind(ck, g_p[k].t1) == wk;
            if(!inA && !inB) break;                                  // wholly before the window
            if(!(inA && inB)) { g_compCut++; break; }                // straddles the window start: partly outside -> left out
           }
         // CRX-010: real and tick volume are different units - only profiles of the SAME type as the newest one are combined
         if(ArraySize(src) > 0 && g_p[k].vtype != g_p[src[0]].vtype) { g_compMixEx++; continue; }
         int m = ArraySize(src); ArrayResize(src, m + 1); src[m] = k;
        }
      if(g_compCut > 0 && g_status == "") g_status = "Composite: the first profile of the window starts before it and was left out (partly outside the window)";
      if(ArraySize(src) > 0) { g_compIdx = MakeComposite(src, ArraySize(src), vCompRows); g_compN = ArraySize(src); }
      if(g_compMixEx > 0 && g_status == "") g_status = "Composite: " + IntegerToString(g_compMixEx) + " profile(s) excluded (different volume type: real vs tick)";
     }
   if(g_realEmpty > 0 && g_status == "") g_status = "Real volume requested but the broker supplies none: " + IntegerToString(g_realEmpty) + " profile(s) empty/invalid";
   //---- 7) anchored profile (exact grid, intrabars)
   if(ancA >= 0)
     {
      double est = 0;
      g_ancIdx = BuildOne(ancA, last, 2, 0, g_ancState == 6 ? 200 : 0, -2, est, vAncRows, false);
     }
   //---- 8) session slots: the latest two sessions of every enabled slot
   for(int s = 0; s < 4; s++)
     {
      if(spn[s] == 0) continue;
      double prevRange = 0;
      for(int q = 0; q < spn[s]; q++)
        {
         int a = sp0[s][q], b = sp1[s][q];
         long pid = CandIdS(a, s + 1);
         double est = prevRange;
         if(est <= 0) { double h0 = cHi[a], l0 = cLo[a]; for(int i = a; i <= b; i++) { if(cHi[i] > h0) h0 = cHi[i]; if(cLo[i] < l0) l0 = cLo[i]; } est = RangeEstimate(a, h0 - l0); }
         int idx = BuildOne(a, b, 3, s, s + 1, pid, est, 0, false, spb[s][q], g_slotS[s], g_slotE[s]);
         prevRange = g_p[idx].hi - g_p[idx].lo;
         if(q == spn[s] - 1) g_msA[s] = idx; else g_msB[s] = idx;
        }
      long formingId = CandIdS(N - 1, s + 1);
      long lastId = CandIdS(sp1[s][spn[s] - 1], s + 1);
      g_msRun[s] = (formingId >= 0 && formingId == lastId);
     }
   g_tot = ArraySize(g_p);
  }

//+------------------------------------------------------------------+
//| Text objects, watermark, panel                                   |
//+------------------------------------------------------------------+
void Label(const string name, const int corner, const int anchor, const int x, const int y,
           const string text, const color clr, const int size, const string font = "Arial")
  {
   if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, corner);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, size);
   ObjectSetString(0, name, OBJPROP_FONT, font);
   ObjectSetString(0, name, OBJPROP_TEXT, text == "" ? " " : text);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 10);
  }

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);
   uint tw = 0, th = 0;
   TextGetSize("crypex", tw, th);
   int wCrypex = (tw > 0) ? (int)tw + 4 : (int)(InpWmSize * 7.0);
   Label(PFX + "WM1", CORNER_LEFT_LOWER, ANCHOR_LEFT_LOWER, InpWmGap, InpWmBottom, "crypex", BRAND_GREEN, InpWmSize, "Arial Black");
   Label(PFX + "WM2", CORNER_LEFT_LOWER, ANCHOR_LEFT_LOWER, InpWmGap + wCrypex, InpWmBottom, ".org", orgCol, InpWmSize, "Arial Black");
  }

string g_rowKey[40];
string g_rowVal[40];
color  g_rowClr[40];
int    g_rows = 0;

void Row(const string k, const string v, const color c)
  {
   if(g_rows >= 40) return;
   g_rowKey[g_rows] = k;
   g_rowVal[g_rows] = v;
   g_rowClr[g_rows] = c;
   g_rows++;
  }

void PaintPanel()
  {
   int W = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), H = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   if(W <= 0 || H <= 0 || g_rows <= 0) return;
   int fs = vPanelFont, pad = 8;
   TextSetFont("Arial", -fs * 10);
   uint tw = 0, th = 0;
   int kw = 0, vw = 0, rh = 0;
   for(int i = 0; i < g_rows; i++)
     {
      TextGetSize(g_rowKey[i], tw, th); if((int)tw > kw) kw = (int)tw; if((int)th > rh) rh = (int)th;
      TextGetSize(g_rowVal[i], tw, th); if((int)tw > vw) vw = (int)tw;
     }
   if(kw <= 0 || rh <= 0) { kw = fs * 14; vw = fs * 24; rh = fs * 2; }
   int keyW = (int)(kw * 1.12) + 16;
   int lineH = MathMax(fs * 2 + 2, rh + 4);
   int pw = pad + keyW + (int)(vw * 1.12) + 18;
   int ph = g_rows * lineH + 10;
   bool right = (InpPanelPos == POS_TOP_RIGHT || InpPanelPos == POS_BOTTOM_RIGHT);
   bool bottom = (InpPanelPos == POS_BOTTOM_LEFT || InpPanelPos == POS_BOTTOM_RIGHT);
   int botMargin = (bottom && !right && InpShowWm) ? 8 + InpWmBottom + (int)(InpWmSize * 2.3) + 6 : 8;
   int x0 = right ? W - 8 - pw : 8;
   int y0 = bottom ? H - botMargin - ph : 8;
   if(x0 < 0) x0 = 0;
   if(y0 < 0) y0 = 0;

   string bgName = PFX + "PBG";
   if(ObjectFind(0, bgName) < 0) ObjectCreate(0, bgName, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, bgName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, bgName, OBJPROP_XDISTANCE, x0);
   ObjectSetInteger(0, bgName, OBJPROP_YDISTANCE, y0);
   ObjectSetInteger(0, bgName, OBJPROP_XSIZE, pw);
   ObjectSetInteger(0, bgName, OBJPROP_YSIZE, ph);
   ObjectSetInteger(0, bgName, OBJPROP_BGCOLOR, InpPanelBg);
   ObjectSetInteger(0, bgName, OBJPROP_COLOR, C'70,76,86');
   ObjectSetInteger(0, bgName, OBJPROP_BORDER_TYPE, 0);
   ObjectSetInteger(0, bgName, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, bgName, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, bgName, OBJPROP_ZORDER, 5);
   for(int i = 0; i < 40; i++)
     {
      string nk = PFX + "PK" + IntegerToString(i);
      string nv = PFX + "PV" + IntegerToString(i);
      if(i >= g_rows) { ObjectDelete(0, nk); ObjectDelete(0, nv); continue; }
      int y = y0 + 6 + i * lineH;
      Label(nk, CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER, x0 + pad, y, g_rowKey[i], i == 0 ? clrWhite : C'150,156,166', fs);
      Label(nv, CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER, x0 + pad + keyW, y, g_rowVal[i], g_rowClr[i], fs, i == 0 ? "Arial Bold" : "Arial");
     }
  }

//+------------------------------------------------------------------+
//| Drawing                                                          |
//+------------------------------------------------------------------+
void Rect(const string name, const datetime t1, const double p1, const datetime t2, const double p2, const color c)
  {
   if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_RECTANGLE, 0, t1, p1, t2, p2);
   ObjectSetInteger(0, name, OBJPROP_TIME, 0, t1);
   ObjectSetDouble(0, name, OBJPROP_PRICE, 0, p1);
   ObjectSetInteger(0, name, OBJPROP_TIME, 1, t2);
   ObjectSetDouble(0, name, OBJPROP_PRICE, 1, p2);
   ObjectSetInteger(0, name, OBJPROP_COLOR, c);
   ObjectSetInteger(0, name, OBJPROP_FILL, true);
   ObjectSetInteger(0, name, OBJPROP_BACK, true);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

void HLine(const string name, const datetime t1, const datetime t2, const double p, const color c, const int w, const ENUM_LINE_STYLE st)
  {
   if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_TREND, 0, t1, p, t2, p);
   ObjectSetInteger(0, name, OBJPROP_TIME, 0, t1);
   ObjectSetDouble(0, name, OBJPROP_PRICE, 0, p);
   ObjectSetInteger(0, name, OBJPROP_TIME, 1, t2);
   ObjectSetDouble(0, name, OBJPROP_PRICE, 1, p);
   ObjectSetInteger(0, name, OBJPROP_COLOR, c);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, w);
   ObjectSetInteger(0, name, OBJPROP_STYLE, st);
   ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false);
   ObjectSetInteger(0, name, OBJPROP_BACK, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

void Txt(const string name, const datetime t, const double p, const string text, const color c)
  {
   if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_TEXT, 0, t, p);
   ObjectSetInteger(0, name, OBJPROP_TIME, 0, t);
   ObjectSetDouble(0, name, OBJPROP_PRICE, 0, p);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_COLOR, c);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, vLabelSize);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_LEFT);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

void DrawProfile(const int k, const bool isCur, const bool isLatestDone, const int wBars, int &nodeLeft)
  {
   string tag = PFX + "PR" + IntegerToString(k) + "_";
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   if(n <= 0 || g_p[k].poc == EMPTY_VALUE) return;
   datetime tL = g_p[k].t0;
   datetime tR = (isCur || (isLatestDone && InpExtendPrev)) ? cTm[N - 1] + (datetime)((long)g_chartSec * vLineExt) : g_p[k].t1 + g_chartSec;
   datetime tEnd = g_p[k].t1 + g_chartSec;
   long bg = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   color bgc = (color)bg;
   if(InpShadeVA)
      Rect(tag + "VA", tL, g_p[k].vah, tR, g_p[k].val, Blend(InpVaLineColor, bgc, 93));
   if(InpShowHist)
     {
      double mx = 0;
      for(int i = 0; i < n; i++) mx = MathMax(mx, g_up[s0 + i] + g_dn[s0 + i]);
      bool leftDir = (InpDir == DIR_LEFT_RIGHT);
      datetime anchor = leftDir ? tL : tEnd;
      if(mx > 0)
         for(int i = 0; i < n; i++)
           {
            double tot = g_up[s0 + i] + g_dn[s0 + i];
            int len = (int)MathRound(tot / mx * wBars);
            if(len < 1) continue;
            bool inVA = (i >= g_p[k].vaLo && i <= g_p[k].vaHi);
            double yT = g_p[k].off + (g_p[k].base + i + 1) * g_p[k].rs;
            double yB = g_p[k].off + (g_p[k].base + i) * g_p[k].rs;
            string nm = tag + "H" + IntegerToString(i);
            if(InpSplitUD)
              {
               int upLen = (int)MathRound(g_up[s0 + i] / mx * wBars);
               if(upLen > len) upLen = len;
               int dnLen = len - upLen;
               color cU = (i == g_p[k].pocI) ? InpPocRowColor : Blend(InpUpColor, bgc, inVA ? vInTransp : vOutTransp);
               color cD = (i == g_p[k].pocI) ? InpPocRowColor : Blend(InpDnColor, bgc, inVA ? vInTransp : vOutTransp);
               long ps = g_chartSec;
               if(leftDir)
                 {
                  if(upLen > 0) Rect(nm + "u", anchor, yT, anchor + (datetime)(ps * upLen), yB, cU);
                  if(dnLen > 0) Rect(nm + "d", anchor + (datetime)(ps * upLen), yT, anchor + (datetime)(ps * len), yB, cD);
                 }
               else
                 {
                  if(upLen > 0) Rect(nm + "u", anchor - (datetime)(ps * upLen), yT, anchor, yB, cU);
                  if(dnLen > 0) Rect(nm + "d", anchor - (datetime)(ps * len), yT, anchor - (datetime)(ps * upLen), yB, cD);
                 }
              }
            else
              {
               color cS = (i == g_p[k].pocI) ? InpPocRowColor : Blend(inVA ? InpVaColor : InpOutColor, bgc, inVA ? vInTransp : vOutTransp);
               long ps = g_chartSec;
               if(leftDir) Rect(nm + "s", anchor, yT, anchor + (datetime)(ps * len), yB, cS);
               else        Rect(nm + "s", anchor - (datetime)(ps * len), yT, anchor, yB, cS);
              }
           }
     }
   string pre = isCur ? "" : "p";
   if(InpShowPoc) HLine(tag + "POC", tL, tR, g_p[k].poc, InpPocColor, vPocWidth, STYLE_SOLID);
   if(InpShowVA)
     {
      HLine(tag + "VAH", tL, tR, g_p[k].vah, InpVaLineColor, vVaLineWidth, STYLE_SOLID);
      HLine(tag + "VAL", tL, tR, g_p[k].val, InpVaLineColor, vVaLineWidth, STYLE_SOLID);
     }
   if(InpShowLabels && (isCur || isLatestDone))
     {
      if(InpShowPoc) Txt(tag + "TPOC", tR, g_p[k].poc, " " + pre + "POC " + Px(g_p[k].poc), InpPocColor);
      if(InpShowVA)
        {
         Txt(tag + "TVAH", tR, g_p[k].vah, " " + pre + "VAH " + Px(g_p[k].vah), InpVaLineColor);
         Txt(tag + "TVAL", tR, g_p[k].val, " " + pre + "VAL " + Px(g_p[k].val), InpVaLineColor);
        }
     }
   if(InpNodes && nodeLeft > 0)
     {
      datetime nx1 = g_p[k].t0 + (datetime)((long)g_chartSec * MathMax(wBars, 8));
      long bgn = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
      if(InpNodeMode == NODE_ADV)
        {
         SNode hv[], lv[];
         double sm[];
         AdvNodes(k, hv, lv, sm);
         for(int q = 0; q < ArraySize(hv); q++)
           {
            double yy = g_p[k].off + (g_p[k].base + hv[q].idx + 0.5) * g_p[k].rs;
            if(InpNodeZones)
               Rect(tag + "NHZ" + IntegerToString(q), g_p[k].t0, g_p[k].off + (g_p[k].base + hv[q].zh + 1) * g_p[k].rs, nx1, g_p[k].off + (g_p[k].base + hv[q].zl) * g_p[k].rs, Blend(InpHvnColor, (color)bgn, 86));
            HLine(tag + "NH" + IntegerToString(q), g_p[k].t0, nx1, yy, InpHvnColor, 1, STYLE_DASH);
            Txt(tag + "NHT" + IntegerToString(q), nx1, yy, " HVN", InpHvnColor);
           }
         for(int q = 0; q < ArraySize(lv); q++)
           {
            double yy = g_p[k].off + (g_p[k].base + lv[q].idx + 0.5) * g_p[k].rs;
            if(InpNodeZones)
               Rect(tag + "NLZ" + IntegerToString(q), g_p[k].t0, g_p[k].off + (g_p[k].base + lv[q].zh + 1) * g_p[k].rs, nx1, g_p[k].off + (g_p[k].base + lv[q].zl) * g_p[k].rs, Blend(InpLvnColor, (color)bgn, 88));
            HLine(tag + "NL" + IntegerToString(q), g_p[k].t0, nx1, yy, InpLvnColor, 1, STYLE_DOT);
            Txt(tag + "NLT" + IntegerToString(q), nx1, yy, " LVN", InpLvnColor);
           }
        }
      else
        {
         int hv[], lv[];
         FindNodes(k, hv, lv);
         for(int q = 0; q < ArraySize(hv); q++)
           {
            double yy = g_p[k].off + (g_p[k].base + hv[q] + 0.5) * g_p[k].rs;
            HLine(tag + "NH" + IntegerToString(q), g_p[k].t0, nx1, yy, InpHvnColor, 1, STYLE_DASH);
            Txt(tag + "NHT" + IntegerToString(q), nx1, yy, " HVN", InpHvnColor);
           }
         for(int q = 0; q < ArraySize(lv); q++)
           {
            double yy = g_p[k].off + (g_p[k].base + lv[q] + 0.5) * g_p[k].rs;
            HLine(tag + "NL" + IntegerToString(q), g_p[k].t0, nx1, yy, InpLvnColor, 1, STYLE_DOT);
            Txt(tag + "NLT" + IntegerToString(q), nx1, yy, " LVN", InpLvnColor);
           }
        }
      nodeLeft--;
     }
  }

// composite / anchored / session profile: POC, VAH, VAL lines (+ optional single-colour histogram)
void DrawSpecial(const int k, const string tag, const color c, const string label, const bool hist, const bool fromRight, const int wBars, const bool shadeVa)
  {
   if(k < 0 || !g_p[k].valid || g_p[k].poc == EMPTY_VALUE) return;
   int n = g_p[k].rowN, s0 = g_p[k].rowStart;
   datetime tL = g_p[k].t0;
   datetime tR = cTm[N - 1] + (datetime)((long)g_chartSec * vLineExt);
   long bg = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   if(shadeVa) Rect(tag + "VA", tL, g_p[k].vah, tR, g_p[k].val, Blend(c, (color)bg, 92));
   if(hist)
     {
      double mx = 0;
      for(int i = 0; i < n; i++) mx = MathMax(mx, g_up[s0 + i] + g_dn[s0 + i]);
      datetime anchor = fromRight ? tR : tL;
      if(mx > 0)
         for(int i = 0; i < n; i++)
           {
            int len = (int)MathRound((g_up[s0 + i] + g_dn[s0 + i]) / mx * wBars);
            if(len < 1) continue;
            bool inVA = (i >= g_p[k].vaLo && i <= g_p[k].vaHi);
            double yT = g_p[k].off + (g_p[k].base + i + 1) * g_p[k].rs, yB = g_p[k].off + (g_p[k].base + i) * g_p[k].rs;
            color cc = (i == g_p[k].pocI) ? c : Blend(c, (color)bg, inVA ? 45 : 75);
            long ps = g_chartSec;
            if(fromRight) Rect(tag + "H" + IntegerToString(i), anchor - (datetime)(ps * len), yT, anchor, yB, cc);
            else          Rect(tag + "H" + IntegerToString(i), anchor, yT, anchor + (datetime)(ps * len), yB, cc);
           }
     }
   HLine(tag + "POC", tL, tR, g_p[k].poc, c, 2, STYLE_SOLID);
   HLine(tag + "VAH", tL, tR, g_p[k].vah, c, 1, STYLE_DASH);
   HLine(tag + "VAL", tL, tR, g_p[k].val, c, 1, STYLE_DASH);
   if(InpShowLabels)
     {
      Txt(tag + "TPOC", tR, g_p[k].poc, " " + label + " POC " + Px(g_p[k].poc), c);
      Txt(tag + "TVAH", tR, g_p[k].vah, " " + label + " VAH " + Px(g_p[k].vah), c);
      Txt(tag + "TVAL", tR, g_p[k].val, " " + label + " VAL " + Px(g_p[k].val), c);
     }
  }

void DrawIB(const int k)
  {
   if(k < 0 || g_p[k].ibH == EMPTY_VALUE || g_p[k].ibL == EMPTY_VALUE) return;
   string tag = PFX + "PRIB_";
   datetime tL = g_p[k].t0, tR = cTm[N - 1] + (datetime)((long)g_chartSec * vLineExt);
   double h = g_p[k].ibH, l = g_p[k].ibL, r = h - l;
   HLine(tag + "H", tL, tR, h, InpIBColor, 2, STYLE_SOLID);
   HLine(tag + "L", tL, tR, l, InpIBColor, 2, STYLE_SOLID);
   Txt(tag + "TH", tR, h, " IBH " + Px(h), InpIBColor);
   Txt(tag + "TL", tR, l, " IBL " + Px(l), InpIBColor);
   if(InpIBExt && r > 0)
      for(int m = 1; m <= 2; m++)
        {
         HLine(tag + "EH" + IntegerToString(m), tL, tR, h + m * r, InpIBColor, 1, STYLE_DOT);
         HLine(tag + "EL" + IntegerToString(m), tL, tR, l - m * r, InpIBColor, 1, STYLE_DOT);
         Txt(tag + "TEH" + IntegerToString(m), tR, h + m * r, " IB+" + IntegerToString(m) + "x", InpIBColor);
         Txt(tag + "TEL" + IntegerToString(m), tR, l - m * r, " IB-" + IntegerToString(m) + "x", InpIBColor);
        }
  }

void AddLv(double &pr[], string &nm[], string &sk[], int &cnt, const double price, const string name, const string key)
  {
   if(price == EMPTY_VALUE) return;
   ArrayResize(pr, cnt + 1); ArrayResize(nm, cnt + 1); ArrayResize(sk, cnt + 1);
   pr[cnt] = price; nm[cnt] = name; sk[cnt] = key; cnt++;
  }

double AtrN(const int period)
  {
   double s = 0;
   int cnt = 0;
   for(int i = N - 2; i >= 1 && cnt < period; i--, cnt++)
      s += MathMax(cHi[i] - cLo[i], MathMax(MathAbs(cHi[i] - cCl[i - 1]), MathAbs(cLo[i] - cCl[i - 1])));
   return cnt > 0 ? s / cnt : 0.0;
  }

int g_confZones = 0;

// key levels that overlap: previous VA, composite, anchored, IB, active naked POCs and HVN centres
void PaintConfluence(const int done, const int cur)
  {
   double pr[];
   string nm[], sk[];
   ArrayResize(pr, 0); ArrayResize(nm, 0); ArrayResize(sk, 0);
   int cnt = 0;
   if(done >= 0) { AddLv(pr, nm, sk, cnt, g_p[done].poc, "pPOC", "P" + IntegerToString(done)); AddLv(pr, nm, sk, cnt, g_p[done].vah, "pVAH", "P" + IntegerToString(done)); AddLv(pr, nm, sk, cnt, g_p[done].val, "pVAL", "P" + IntegerToString(done)); }
   if(g_compIdx >= 0) { AddLv(pr, nm, sk, cnt, g_p[g_compIdx].poc, "cPOC", "C"); AddLv(pr, nm, sk, cnt, g_p[g_compIdx].vah, "cVAH", "C"); AddLv(pr, nm, sk, cnt, g_p[g_compIdx].val, "cVAL", "C"); }
   if(g_ancIdx >= 0) { AddLv(pr, nm, sk, cnt, g_p[g_ancIdx].poc, "aPOC", "A"); }
   int refIB = cur >= 0 ? cur : done;
   if(InpIB && refIB >= 0) { AddLv(pr, nm, sk, cnt, g_p[refIB].ibH, "IBH", "P" + IntegerToString(refIB)); AddLv(pr, nm, sk, cnt, g_p[refIB].ibL, "IBL", "P" + IntegerToString(refIB)); }
   if(InpNaked && done >= 0)
      for(int k = done; k >= 0; k--)
         if(g_p[k].naked && (InpNakedMaxAge <= 0 || (N - 1 - g_p[k].i1) <= InpNakedMaxAge)) AddLv(pr, nm, sk, cnt, g_p[k].poc, "nPOC", "P" + IntegerToString(k));
   int hvSrc[2];
   hvSrc[0] = cur; hvSrc[1] = done;
   for(int z = 0; z < 2; z++)
     {
      int k = hvSrc[z];
      if(k < 0 || (z == 1 && k == hvSrc[0])) continue;
      SNode hv[], lv[];
      double sm[];
      AdvNodes(k, hv, lv, sm);
      for(int q = 0; q < ArraySize(hv); q++) AddLv(pr, nm, sk, cnt, g_p[k].off + (g_p[k].base + hv[q].idx + 0.5) * g_p[k].rs, "HVN", "P" + IntegerToString(k));
     }
   double tol = vConfTol * AtrN(14);
   double zlo[], zhi[];
   int zn[];
   string zs[];
   int nz = tol > 0 ? ClusterLevels(pr, nm, sk, cnt, tol, vConfMin, zlo, zhi, zn, zs) : 0;
   g_confZones = nz;
   if(nz == 0) return;
   double px = cCl[N - 2];
   int order[];
   ArrayResize(order, nz);
   for(int i = 0; i < nz; i++) order[i] = i;
   for(int a = 1; a < nz; a++)
     {
      int v = order[a], b = a - 1;
      while(b >= 0 && MathAbs((zlo[order[b]] + zhi[order[b]]) / 2 - px) > MathAbs((zlo[v] + zhi[v]) / 2 - px)) { order[b + 1] = order[b]; b--; }
      order[b + 1] = v;
     }
   long bg = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   datetime tL = cTm[MathMax(0, N - 80)], tR = cTm[N - 1] + (datetime)((long)g_chartSec * vLineExt);
   for(int q = 0; q < nz && q < vConfMax; q++)
     {
      int z = order[q];
      double pad = tol * 0.25;
      Rect(PFX + "PRCF" + IntegerToString(q), tL, zhi[z] + pad, tR, zlo[z] - pad, Blend(InpConfColor, (color)bg, 78));
      Txt(PFX + "PRCFT" + IntegerToString(q), tR, (zlo[z] + zhi[z]) / 2, " Confluence x" + IntegerToString(zn[z]) + " " + zs[z], InpConfColor);
     }
  }

void PaintAll()
  {
   ObjectsDeleteAll(0, PFX + "PR");
   ObjectsDeleteAll(0, PFX + "NK");
   g_confZones = 0;
   if(g_tot == 0) return;
   int cur = g_curIdx, done = g_doneIdx;
   int wBars = 10;
   if(g_np > 0)
     {
      int refA = cur >= 0 ? cur : done;
      int span = g_p[refA].i1 - g_p[refA].i0 + 1;
      if(cur >= 0 && done >= 0) span = MathMax(span, g_p[done].i1 - g_p[done].i0 + 1);
      if(g_effPeriod == VP_ROLL) span = vRollN;
      wBars = MathMax(3, (int)MathRound(span * vWidthPct / 100.0));
      int nodeLeft = vNodeProfs;
      if(cur >= 0 && g_p[cur].valid) DrawProfile(cur, true, false, wBars, nodeLeft);
      if(g_effPeriod != VP_ROLL && done >= 0)
        {
         for(int k = done; k >= MathMax(0, done - vKeepPrev + 1) && vKeepPrev > 0; k--)
            if(g_p[k].valid) DrawProfile(k, false, k == done, wBars, nodeLeft);
         if(InpNaked)
           {
            int shown = 0, consumed = 0;
            for(int k = done; k >= 0 && (shown < vNakedMax || consumed < vNakedConsumed); k--)
              {
               if(!g_p[k].valid || g_p[k].poc == EMPTY_VALUE) continue;
               int age = N - 1 - g_p[k].i1;
               datetime tR = cTm[N - 1] + (datetime)((long)g_chartSec * vLineExt);
               string nm = PFX + "NK" + IntegerToString(k);
               string dstr = TimeToString(g_p[k].t0, TIME_DATE);
               if(g_p[k].naked)
                 {
                  if(shown >= vNakedMax || (InpNakedMaxAge > 0 && age > InpNakedMaxAge)) continue;
                  double dist = (g_p[k].poc - cCl[N - 2]) / cCl[N - 2] * 100.0;
                  HLine(nm, g_p[k].t1, tR, g_p[k].poc, InpNakedColor, 1, STYLE_DASH);
                  Txt(nm + "T", tR, g_p[k].poc, " nPOC " + dstr + " " + Px(g_p[k].poc) + " · " + IntegerToString(age) + " bars · " + DoubleToString(dist, 2) + "%" + (g_p[k].prov ? " · provisional touch" : ""), InpNakedColor);
                  shown++;
                 }
               else if(g_p[k].touchBar >= 0 && consumed < vNakedConsumed)
                 {
                  long bgc = ChartGetInteger(0, CHART_COLOR_BACKGROUND);
                  color fc = Blend(InpNakedColor, (color)bgc, 65);
                  HLine(nm, g_p[k].t1, cTm[g_p[k].touchBar], g_p[k].poc, fc, 1, STYLE_DOT);
                  Txt(nm + "T", cTm[g_p[k].touchBar], g_p[k].poc, " touched " + dstr + " " + Px(g_p[k].poc), fc);
                  consumed++;
                 }
              }
           }
        }
      int refIB = cur >= 0 ? cur : done;
      if(InpIB && refIB >= 0) DrawIB(refIB);
     }
   if(g_compIdx >= 0) DrawSpecial(g_compIdx, PFX + "PRCP_", InpCompColor, "Comp", InpCompHist, true, MathMax(8, wBars), false);
   if(g_ancIdx >= 0)  DrawSpecial(g_ancIdx, PFX + "PRAN_", InpAncColor, "Anch", InpAncHist, false, MathMax(8, wBars), false);
   for(int s = 0; s < 4; s++)
     {
      if(g_msB[s] >= 0) DrawSpecial(g_msB[s], PFX + "PRMB" + IntegerToString(s) + "_", Blend(SlotColor(s), (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND), 40), "p" + SlotName(s), false, false, 8, false);
      if(g_msA[s] >= 0) DrawSpecial(g_msA[s], PFX + "PRMA" + IntegerToString(s) + "_", SlotColor(s), SlotName(s), InpMsHist, false, MathMax(8, wBars), false);
     }
   if(InpConf) PaintConfluence(done, cur);
  }

//+------------------------------------------------------------------+
//| Panel content and alerts                                         |
//+------------------------------------------------------------------+
void Notify(const string msg)
  {
   if(InpAlertPopup) Alert(msg);
   if(InpAlertPush)  { if(!SendNotification(msg)) Print("Crypex VP: push notification failed, error ", GetLastError()); }
   if(InpAlertMail)  { if(!SendMail("Crypex Volume Profile", msg)) Print("Crypex VP: e-mail failed, error ", GetLastError()); }
  }

// CRX-018: levels of the latest FINISHED profile are only alertable when that profile is valid and the data basis is usable
bool AlertLevelsOk()
  {
   if(g_effPeriod == VP_ROLL || g_doneIdx < 0 || g_strictNA) return false;
   if(!g_p[g_doneIdx].valid) return false;
   if(InpAlertBlockPoor && QualityPoor(g_doneIdx)) return false;          // CRX-029: mathematically valid is not the same as data-quality acceptable
   return true;
  }

string AlertTag(const int pv)
  {
   return QualityPoor(pv) ? " [previous profile data INCOMPLETE: " + QualityText(pv) + "]" : "";
  }

// confirmed-bar alerts against the latest FINISHED profile; the very first evaluation only arms the detector (no historical alert)
void CheckAlerts()
  {
   if(g_effPeriod == VP_ROLL || N < 5) return;
   if(InpAlertHealth)
     {
      int hs = (g_strictNA ? 1 : 0) | (g_tzState == 2 ? 2 : 0) | ((g_doneIdx >= 0 && QualityPoor(g_doneIdx)) ? 4 : 0);
      if(hs != g_healthSeen && g_alertInit)
        {
         if((hs & 1) != 0 && (g_healthSeen & 1) == 0) Notify(_Symbol + ": Crypex VP - Strict intrabars UNAVAILABLE, profiles are not built");
         if((hs & 2) != 0 && (g_healthSeen & 2) == 0) Notify(_Symbol + ": Crypex VP - server time model does NOT match the terminal clock; session/day boundaries unreliable");
         if((hs & 4) != 0 && (g_healthSeen & 4) == 0) Notify(_Symbol + ": Crypex VP - previous profile built from INCOMPLETE data: " + QualityText(g_doneIdx));
        }
      g_healthSeen = hs;
     }
   if(cTm[N - 2] == g_lastAlertBar) return;
   g_lastAlertBar = cTm[N - 2];
   if(!g_alertInit) { g_alertInit = true; return; }
   if(PeriodId(cTm[N - 2]) != PeriodId(cTm[N - 3]))                          // do not compare closes across a period change
     {
      if(InpAlertNewPeriod && AlertLevelsOk())
         Notify(_Symbol + " " + EnumToString(_Period) + ": new period - previous POC " + Px(g_p[g_doneIdx].poc) + " VAH " + Px(g_p[g_doneIdx].vah) + " VAL " + Px(g_p[g_doneIdx].val) + AlertTag(g_doneIdx));
      return;
     }
   if(InpAlertMode != AL_CLOSE || !AlertLevelsOk()) return;
   int pv = g_doneIdx;
   double c1 = cCl[N - 2], c0 = cCl[N - 3];
   double lv[3];
   string nm[3] = {"POC", "VAH", "VAL"};
   lv[0] = g_p[pv].poc; lv[1] = g_p[pv].vah; lv[2] = g_p[pv].val;
   for(int q = 0; q < 3; q++)
     {
      if(lv[q] == EMPTY_VALUE) continue;
      if(c0 <= lv[q] && c1 > lv[q]) Notify(_Symbol + " " + EnumToString(_Period) + ": candle CLOSED above previous " + nm[q] + " " + Px(lv[q]) + AlertTag(pv));
      if(c0 >= lv[q] && c1 < lv[q]) Notify(_Symbol + " " + EnumToString(_Period) + ": candle CLOSED below previous " + nm[q] + " " + Px(lv[q]) + AlertTag(pv));
     }
  }

// live-cross alerts: evaluated on every tick, at most once per level/direction per bar (a live cross can repeat while the candle is open)
void CheckLive(const double px)
  {
   if(InpAlertMode != AL_LIVE || !g_alertInit || !AlertLevelsOk() || N < 2 || px <= 0) return;
   if(g_livePx <= 0) { g_livePx = px; return; }
   int pv = g_doneIdx;
   double lv[3];
   string nm[3] = {"POC", "VAH", "VAL"};
   lv[0] = g_p[pv].poc; lv[1] = g_p[pv].vah; lv[2] = g_p[pv].val;
   for(int q = 0; q < 3; q++)
     {
      if(lv[q] == EMPTY_VALUE) continue;
      if(g_livePx <= lv[q] && px > lv[q] && g_liveBar[q * 2] != cTm[N - 1]) { g_liveBar[q * 2] = cTm[N - 1]; Notify(_Symbol + " " + EnumToString(_Period) + ": price crossed ABOVE previous " + nm[q] + " " + Px(lv[q]) + " (live, candle not closed)" + AlertTag(pv)); }
      if(g_livePx >= lv[q] && px < lv[q] && g_liveBar[q * 2 + 1] != cTm[N - 1]) { g_liveBar[q * 2 + 1] = cTm[N - 1]; Notify(_Symbol + " " + EnumToString(_Period) + ": price crossed BELOW previous " + nm[q] + " " + Px(lv[q]) + " (live, candle not closed)" + AlertTag(pv)); }
     }
   g_livePx = px;
  }

string VolKindText()
  {
   if(g_nReal > 0 && g_nTick == 0) return "Real volume";
   if(g_nReal == 0) return "Tick volume (approx.)";
   return "Mixed (real / tick per profile)";
  }

void BuildPanel()
  {
   g_rows = 0;
   color gry = C'150,156,166', grn = C'38,166,91', red = C'239,83,80', org = C'255,152,0';
   string vk = VolKindText();
   Row("Crypex VP", _Symbol, clrWhite);
   if(InpShowTickRow) Row("Tick size (eff.)", DoubleToString(g_tick, 8) + " · " + g_tickSrc + " (raw " + DoubleToString(g_tickRaw, 8) + ")", clrWhite);
   if(UsesTzConversion())
     {
      string dr = "no DST";
      if(InpServerDst == DST_US) dr = "US DST";
      else if(InpServerDst == DST_EU) dr = "EU DST";
      else if(InpServerDst == DST_MANUAL) dr = "manual schedule (" + IntegerToString(g_schN) + " entries)";
      string sgS = vServerOffset >= 0 ? "+" : "";
      string tzs = "server = UTC" + sgS + DoubleToString(vServerOffset, 1) + " winter · " + dr;
      if(g_tzState == 1) Row("Time basis", tzs + " · matches terminal clock", grn);
      else if(g_tzState == 2)
        {
         string sgO = g_tzObs >= 0 ? "+" : "", sgM = g_tzMdl >= 0 ? "+" : "";
         Row("Time basis", tzs + " · MISMATCH: terminal reports UTC" + sgO + DoubleToString(g_tzObs, 2) + ", model UTC" + sgM + DoubleToString(g_tzMdl, 2) + " - session/day boundaries are NOT reliable", red);
        }
      else Row("Time basis", tzs + " · ASSUMED (not verifiable now) - boundaries are only as right as this setting", org);
     }
   Row("Volume type", vk, vk == "Real volume" ? grn : org);
   string src = (g_effPeriod == VP_ROLL || !g_useLtf) ? "Chart bars" : StringSubstr(EnumToString(g_ltf), 7) + " intrabars";
   if(g_strictNA) src = "UNAVAILABLE (Strict, no lower timeframe)";
   else if(g_effPeriod == VP_ROLL && InpVolSrc == SRC_STRICT) src = "Chart bars (Rolling mode has no intrabars)";
   long cv = g_ltfBars + g_fbBars + g_skipBars;
   if(g_effPeriod != VP_ROLL && g_useLtf && cv > 0)
     {
      src += " · " + IntegerToString((int)(g_ltfBars * 100 / cv)) + "% intrabar";
      if(g_cvExp > 0) src += " · time cov " + DoubleToString(100.0 * g_cvGot / g_cvExp, 1) + "%" + (g_cvVolChart > 0 ? " · vol cov " + DoubleToString(100.0 * g_cvVolIn / g_cvVolChart, 1) + "%" : "");
      if(g_cvSparse > 0)  src += " · " + IntegerToString((int)g_cvSparse) + " sparse (no-trade minutes)";
      if(g_cvPartial > 0) src += " · " + IntegerToString((int)g_cvPartial) + " PARTIAL (Strict, volume mismatch)";
      if(g_cvTypeMis > 0) src += " · " + IntegerToString((int)g_cvTypeMis) + " volume-type mismatch";
      if(g_fbBars > 0)   src += " · " + IntegerToString((int)(g_fbBars * 100 / cv)) + "% chart-bar";
      if(g_skipBars > 0) src += " · " + IntegerToString((int)(g_skipBars * 100 / cv)) + "% missing";
     }
   Row("Source", src, clrWhite);
   string per = (InpPeriod == VP_AUTO ? "Auto → " : "") + (g_effPeriod == VP_DAY ? "Day" : g_effPeriod == VP_WEEK ? "Week" : g_effPeriod == VP_MONTH ? "Month" : g_effPeriod == VP_SESSION ? "Session " + InpSessStart + "-" + InpSessEnd : "Last " + IntegerToString(vRollN) + " bars");
   Row("Period", per, clrWhite);
   double px = Bid;
   if(g_curIdx >= 0)
     {
      int c = g_curIdx;
      Row("Rows", IntegerToString(g_p[c].rowN) + " × " + DoubleToString(g_p[c].rs, _Digits), clrWhite);
      if(g_effPeriod != VP_ROLL) Row("Data · current", QualityText(c), QualityPoor(c) ? org : grn);
      Row("POC", Px(g_p[c].poc), InpPocColor);
      Row("VAH / VAL", Px(g_p[c].vah) + " / " + Px(g_p[c].val), InpVaLineColor);
      string where = "—"; color wc = gry;
      if(g_p[c].vah != EMPTY_VALUE)
        {
         where = px > g_p[c].vah ? "Above VA" : (px < g_p[c].val ? "Below VA" : "Inside VA");
         wc = px > g_p[c].vah ? grn : (px < g_p[c].val ? red : gry);
         if(g_p[c].poc > 0) where += " · " + DoubleToString((px - g_p[c].poc) / g_p[c].poc * 100.0, 2) + "% from POC";
        }
      Row("Price", where, wc);
      if(g_p[c].total > 0)
        {
         double upP = g_p[c].upTotal / g_p[c].total * 100.0;
         Row("Est. up/down vol", "Up " + DoubleToString(upP, 0) + "% / Down " + DoubleToString(100 - upP, 0) + "%", upP >= 52 ? grn : (upP <= 48 ? red : gry));
        }
     }
   else if(g_np > 0) Row("Current", "waiting for the first closed bar", org);
   if(g_effPeriod != VP_ROLL && g_doneIdx >= 0)
     {
      int pv = g_doneIdx;
      Row("Prev POC", Px(g_p[pv].poc), InpPocColor);
      Row("Prev VAH / VAL", Px(g_p[pv].vah) + " / " + Px(g_p[pv].val), InpVaLineColor);
      int cnt = 0; double nearest = EMPTY_VALUE;
      for(int k = 0; k <= g_doneIdx; k++)
         if(g_p[k].naked && (InpNakedMaxAge <= 0 || (N - 1 - g_p[k].i1) <= InpNakedMaxAge))
           {
            cnt++;
            if(nearest == EMPTY_VALUE || MathAbs(g_p[k].poc - px) < MathAbs(nearest - px)) nearest = g_p[k].poc;
           }
      Row("Naked POCs", cnt == 0 ? "None" : IntegerToString(cnt) + " · nearest " + Px(nearest), InpNakedColor);
     }
   //---- analytics describing the current profile (or the latest finished one) against the previous profile
   int A = g_curIdx >= 0 ? g_curIdx : g_doneIdx;
   if(g_effPeriod != VP_ROLL && A >= 1 && g_p[A].poc != EMPTY_VALUE && g_p[A - 1].poc != EMPTY_VALUE)
     {
      int Bp = A - 1;
      if(InpMigration)
        {
         double d = g_p[A].poc - g_p[Bp].poc;
         string st = MigrationState(d, g_p[A].rs);
         Row("POC migration", (d >= 0 ? "+" : "") + DoubleToString(d / g_tick, 0) + " ticks (" + (d >= 0 ? "+" : "") + DoubleToString(d / g_p[Bp].poc * 100.0, 2) + "%) · " + st, st == "Stable" ? gry : (d > 0 ? grn : red));
        }
      if(InpVaShift)
        {
         string vs = VaShiftText(g_p[A].val, g_p[A].vah, g_p[Bp].val, g_p[Bp].vah);
         Row("VA vs previous", vs, vs == "Higher value" ? grn : (vs == "Lower value" ? red : clrWhite));
        }
     }
   if(InpMigration && InpShowDev && g_curIdx >= 0 && N >= 4)
     {
      double d1 = BufPoc[1], d0 = BufPoc[2];
      if(d1 != EMPTY_VALUE && d0 != EMPTY_VALUE && d1 != 0 && d0 != 0)
         Row("Dev POC last bar", (d1 - d0 >= 0 ? "+" : "") + DoubleToString((d1 - d0) / g_tick, 0) + " ticks", d1 > d0 ? grn : (d1 < d0 ? red : gry));
     }
   if(InpShape && A >= 0) Row("Shape (heuristic)", ShapeOf(A), clrWhite);
   if(InpRvol && g_curIdx >= 0)
     {
      int used;
      double rv = RvolOf(g_curIdx, used);
      Row("Rel. volume", rv < 0 ? "n/a (needs 2+ previous)" : DoubleToString(rv, 2) + "x · avg of " + IntegerToString(used), rv >= 1.5 ? grn : (rv < 0 ? gry : (rv < 0.7 ? org : clrWhite)));
     }
   if(InpIB && A >= 0 && g_p[A].ibH != EMPTY_VALUE)
     {
      double r = g_p[A].ibH - g_p[A].ibL;
      Row("IB high / low", Px(g_p[A].ibH) + " / " + Px(g_p[A].ibL) + (g_p[A].ibEst ? " (approx.: partial candle without intrabars left out)" : ""), InpIBColor);
      string ibs = "Inside IB";
      if(r > 0 && px > g_p[A].ibH) ibs = "Above IB +" + DoubleToString((px - g_p[A].ibH) / r, 2) + "x";
      else if(r > 0 && px < g_p[A].ibL) ibs = "Below IB -" + DoubleToString((g_p[A].ibL - px) / r, 2) + "x";
      Row("IB range", Px(r) + " · " + ibs, clrWhite);
     }
   if(g_effPeriod != VP_ROLL && g_doneIdx >= 0 && g_doneIdx != g_curIdx) Row("Data · previous", QualityText(g_doneIdx), QualityPoor(g_doneIdx) ? org : grn);
   if(g_compIdx >= 0)
     {
      Row("Composite", "POC " + Px(g_p[g_compIdx].poc) + " · VA " + Px(g_p[g_compIdx].val) + "-" + Px(g_p[g_compIdx].vah) + " · " + IntegerToString(g_compN) + " profiles · " + (g_p[g_compIdx].vtype == 1 ? "real volume" : "tick volume (approx.)") + (g_compMixEx > 0 ? " · " + IntegerToString(g_compMixEx) + " excluded (other type)" : ""), InpCompColor);
      if(g_effPeriod != VP_ROLL) Row("Data · composite", QualityText(g_compIdx), QualityPoor(g_compIdx) ? org : grn);
     }
   if(g_ancIdx >= 0)
     {
      string an = "";
      if(g_ancState == 2) an = " · anchor in a market gap: starts at the next candle (nothing lost)";
      else if(g_ancState == 3) an = " · SNAPPED forward to the next candle open: " + IntegerToString(g_ancLostSec / 60) + " min " + IntegerToString(g_ancLostSec % 60) + " s before it is NOT included (no intrabars)";
      else if(g_ancState == 4) an = " · anchor is before the available history: starts at the first loaded candle";
      else if(g_ancState == 6) an = " · cut exactly at the anchor using intrabars";
      Row("Anchored", "POC " + Px(g_p[g_ancIdx].poc) + " · VA " + Px(g_p[g_ancIdx].val) + "-" + Px(g_p[g_ancIdx].vah) + an, g_ancState == 3 || g_ancState == 4 ? org : InpAncColor);
      Row("Data · anchored", QualityText(g_ancIdx), QualityPoor(g_ancIdx) ? org : grn);
     }
   else if(InpAnc && g_ancState == 5)
      Row("Anchored", "NOT BUILT: the anchor time is after the newest completed candle", org);
   for(int s = 0; s < 4; s++)
     {
      int ms = g_msA[s];
      if(ms < 0 || g_p[ms].poc == EMPTY_VALUE) continue;
      Row(SlotName(s) + (g_msRun[s] ? "" : " (done)"), "POC " + Px(g_p[ms].poc) + " · H/L " + Px(g_p[ms].hi) + "/" + Px(g_p[ms].lo) + " · vol " + DoubleToString(g_p[ms].total, 0), SlotColor(s));
     }
   if(InpConf) Row("Confluence", IntegerToString(g_confZones) + " zone(s)", InpConfColor);
   int refC = g_curIdx >= 0 ? g_curIdx : g_doneIdx;
   string vam = InpVaMethod == VA_TV ? "TV rule" : (InpVaMethod == VA_TARGET1 ? "Target 1-row" : "Pair 2-row");
   Row("VA method", vam + " · " + DoubleToString(vVaPct, 0) + "%" + (refC >= 0 ? " → covers " + DoubleToString(g_p[refC].vaCov, 1) + "%" : ""), clrWhite);
   string note = "OK"; color nc = grn;
   if(vk != "Real volume") { note = "Tick volume ≠ real turnover"; nc = org; }
   if(g_effPeriod == VP_DAY && g_chartSec >= 86400) { note = "Chart TF ≥ period"; nc = red; }
   if(g_effPeriod == VP_WEEK && g_chartSec >= 604800) { note = "Chart TF ≥ period"; nc = red; }
   if(g_clamped > 0) { note = "Data outside grid: " + IntegerToString((int)g_clamped) + " candles clamped"; nc = org; }
   for(int k = 0; k < g_np; k++) if(!g_p[k].valid) { note = "Profile invalid (volume mismatch or data outside its grid)"; nc = red; }
   if(g_status != "") { note = g_status; nc = org; }
   if(g_inNote != "") Row("Inputs", "CORRECTED: " + g_inNote, clrOrange);
   Row("Note", note, nc);
  }

//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_CHART_CHANGE && InpShowPanel && g_rows > 0) PaintPanel();
  }

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 < 10) return 0;
   uint now = GetTickCount();
   datetime curBar = Time[0];
   bool newBar = (curBar != g_lastBar) || prev_calculated == 0;
   if(g_retry && now - g_lastTry > 3000) { newBar = true; }            // history / intrabar data was not ready: rebuild
   if(!newBar && now - g_lastPaint < 1500) return rates_total;
   if(newBar)
     {
      g_status = ""; g_retry = false; g_lastTry = now;
      CheckServerTz();
      if(!LoadChart()) { BuildPanel(); if(InpShowPanel) PaintPanel(); return 0; }
      if(g_strictNA) g_status = "Strict intrabars UNAVAILABLE (no usable lower timeframe): profiles are NOT built from chart bars";
      g_ltfBars = 0; g_fbBars = 0; g_skipBars = 0; g_cvExp = 0; g_cvGot = 0; g_cvSparse = 0; g_cvPartial = 0; g_cvTypeMis = 0; g_cvVolIn = 0; g_cvVolChart = 0;
      BuildProfiles();
      g_volKind = VolKindText();
      g_lastBar = curBar;
      CheckAlerts();
      PaintAll();
     }
   CheckLive(SymbolInfoDouble(_Symbol, SYMBOL_BID));
   BuildPanel();
   if(InpShowPanel) PaintPanel();
   PaintWatermark();
   g_lastPaint = now;
   ChartRedraw();
   return rates_total;
  }
//+------------------------------------------------------------------+

منطق کار

  • پروفایل برای دوره‌ی Day / Week / Month / Session دلخواه یا N کندل آخر (Rolling) ساخته می‌شود. حالت پیش‌فرض Auto است: بر اساس تایم‌فریم خودِ چارت بین این دوره‌ها خودکار جابه‌جا می‌شود تا هیچ‌وقت روی هیچ تایم‌فریمی خالی نماند.
  • حجم هر کندل ریزتر (ریزکندل/اینترابار) به‌صورت یکنواخت روی بازه‌ی High-Low آن پخش و بین ردیف‌های قیمتی تقسیم می‌شود؛ اگر ریزکندل در دسترس نباشد، به‌صورت fallback از حجم خود کندل چارت استفاده می‌شود (هیچ‌وقت هر دو با هم جمع نمی‌شوند).
  • POC (پرحجم‌ترین ردیف) و Value Area (پیش‌فرض ۷۰٪) با سه روش قابل‌انتخاب محاسبه می‌شود: قاعده‌ی مستندشده‌ی TradingView، افزودن تا رسیدن به هدف (۱ ردیف)، یا جفت‌ردیفی (روش CME/Dalton).
  • گره‌های HVN (قله‌ی حجم) و LVN (دره‌ی حجم) با دو روش: Advanced (قله‌های برجسته با حداقل فاصله و ناحیه‌ی هر گره) یا Simple (قله/دره‌ی محلی در یک پنجره).
  • Naked POC: نقطه‌ی POC یک پروفایل تمام‌شده که قیمت هنوز به آن برنگشته؛ با اولین لمس (سایه یا بسته‌شدن، قابل‌انتخاب) به حالت «مصرف‌شده» می‌رود.
  • تحلیل‌های تکمیلی در جدول: مهاجرت POC و جابه‌جایی Value Area نسبت به پروفایل قبلی، حجم نسبی (Rel. volume) نسبت به میانگین پروفایل‌های قبلی در همان بازه‌ی زمانی، و شکل پروفایل (D/P/b/دوقله‌ای) به‌صورت تقریبی و صرفاً عددی.
  • Initial Balance (بازه‌ی ابتدایی دوره)، پروفایل ترکیبی (Composite، بدون شمارش دوباره‌ی حجم روی چند دوره)، پروفایل لنگرشده (Anchored، از یک تاریخ/ساعت دلخواه تا الان) و تا ۴ پروفایل سشن مستقل هم‌زمان (آسیا/لندن/نیویورک/دلخواه) قابل‌فعال‌سازی هستند.
  • نواحی هم‌پوشانی (Confluence): جاهایی که چند سطح مهم (POC/VA قبلی، ترکیبی، لنگرشده، Initial Balance، Naked POC، HVN) روی هم بیفتند، با رواداری بر حسب ATR مشخص می‌شوند.
  • هشدارهای آماده: عبور تأییدشده (بسته‌شدن کندل) یا لحظه‌ای (Live) از POC/VAH/VAL پروفایل قبلی، شروع دوره‌ی جدید، و افت کیفیت داده (نبود ریزکندل، داده‌ی ناقص، یا عدم تطابق ساعت سرور).
  • داده‌ی ناقص هیچ‌وقت به‌عنوان داده‌ی سالم قلمداد نمی‌شود: پروفایلی که به ریزکندل Fallback کرده یا کندل مرزی‌اش کامل نبوده در جدول علامت می‌خورد و هشدارهای سطح روی آن غیرفعال می‌شوند.
  • نسخه‌ی متاتریدر ۵ و ۴: همان ریاضیات POC/Value Area/HVN-LVN/Naked POC/Initial Balance/Composite/Anchored/چندسشن/Confluence را دارد، با همان حالت Auto برای انتخاب دوره. جدول وضعیت در متاتریدر به‌صورت پیش‌فرض خاموش است (از تنظیمات اندیکاتور روشن می‌شود) و در گوشه‌ی دلخواه صفحه ثابت می‌ماند.

تنظیمات مهم

Profile period
Auto (پیش‌فرض، بر اساس تایم‌فریم چارت) یا انتخاب دستی Day/Week/Month/Session/Last N bars
Rows per profile / Row size
تعداد ردیف‌های قیمتی و روش تعیین اندازه‌ی ردیف (خودکار یا تیک ثابت)
Value Area % / method
درصد پوشش Value Area (پیش‌فرض ۷۰٪) و روش محاسبه (TradingView / هدف‌محور / جفت‌ردیفی)
Volume source / Intrabar timeframe
منبع حجم (ریزکندل با fallback، فقط ریزکندل، یا فقط کندل چارت) و تایم‌فریم ریزکندل (خودکار یا دستی)
Histogram / Direction / Split Up-Down
نمایش هیستوگرام، جهت رشد آن و تفکیک رنگ حجم صعودی/نزولی
Nodes (HVN/LVN)
روش تشخیص (Advanced/Simple) و آستانه‌های برجستگی، فاصله و درصد حجم
Naked POC
حداکثر تعداد نمایش، قاعده‌ی لمس (سایه/بسته‌شدن) و حداکثر سن نمایش
Initial Balance / Composite / Anchored / Multi-session
هرکدام جداگانه با تنظیمات خودشان فعال می‌شوند؛ برای شلوغ‌نشدن چارت پیش‌فرض همه خاموش‌اند
Confluence zones
رواداری خوشه‌بندی (ضریبی از ATR۱۴)، حداقل تعداد سطح در هر ناحیه و حداکثر تعداد ناحیه‌ی نمایش‌داده‌شده
Alerts
عبور تأییدشده یا لحظه‌ای از سطوح، شروع دوره‌ی جدید و افت کیفیت داده؛ در متاتریدر پاپ‌آپ، پوش و ایمیل جدا تنظیم می‌شود
Show crypex.org watermark
واترمارک قابل خاموش‌کردن، با ورودی جایگاه، اندازه و شفافیت

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

تحلیل حجم روی هر بازاری که حجم معنادار دارد (ارز دیجیتال با حجم واقعی صرافی، یا فارکس/طلا با حجم تیک به‌عنوان تقریب). مناسب پیداکردن POC و Value Area روز/هفته/ماه جاری و قبلی، ردیابی Naked POCهای برنگشته، و هم‌راستاسازی چند سطح (ترکیبی، لنگرشده، سشن‌ها) با حالت Confluence. حالت Auto باعث می‌شود روی هر تایم‌فریمی که اندیکاتور اضافه شود (حتی دیلی یا هفتگی) چیزی برای نمایش داشته باشد.

نقطه‌ضعف

این ابزار تحلیلی است، نه سیگنال معاملاتی. روی فارکس/CFD حجم از نوع تیک است و در جدول وضعیت مشخص می‌شود. با فعال‌کردن هم‌زمان همه‌ی لایه‌های اختیاری چارت شلوغ می‌شود؛ بهتر است مرحله‌ای روشن شوند.

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

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

  • TradingView: کد را در Pine Editor بچسبانید و Add to chart بزنید. متاتریدر: فایل را در MetaEditor با F7 کامپایل کنید و از Navigator روی چارت بکشید.
  • دوره‌ی پروفایل پیش‌فرض روی Auto است و خودش بر اساس تایم‌فریم چارت بین Day/Week/Month/Last N bars جابه‌جا می‌شود؛ برای انتخاب دستی (مثلاً فقط Day، یا Session دلخواه) از «Profile period» تغییرش دهید.
  • در متاتریدر جدول وضعیت به‌صورت پیش‌فرض خاموش است؛ از تنظیمات اندیکاتور «Show status panel/table» را روشن کنید و جایگاهش را از «Panel position» انتخاب کنید.
  • لایه‌های اختیاری (Initial Balance، پروفایل ترکیبی، لنگرشده، چندسشن، Confluence) پیش‌فرض خاموش‌اند تا چارت شلوغ نشود؛ هر کدام را جدا و مرحله‌ای روشن کنید.
  • اگر «Volume source» روی حالت پیش‌فرض (Fallback to chart bar) باشد و ریزکندل تازه سینک نشده باشد، پروفایل موقتاً از حجم خود کندل چارت استفاده می‌کند و در جدول و هشدار «کیفیت داده» اعلام می‌شود؛ معمولاً با باز نگه‌داشتن چارت روی تایم‌فریم پایین‌تر همان نماد حل می‌شود.

چطور جدول و سطوح را بخوانیم

  • POC پرحجم‌ترین ردیف قیمتی پروفایل است؛ VAH/VAL مرز بالا و پایین Value Area (پیش‌فرض ۷۰٪ حجم) هستند.
  • HVN (نقطه‌چین فیروزه‌ای) ناحیه‌ی تعادل با حجم بالاست و LVN (نقطه‌چین صورتی) ناحیه‌ی عبور سریع با حجم کم؛ قیمت معمولاً در HVN کندتر و در LVN سریع‌تر حرکت می‌کند.
  • Naked POC خط‌چین نارنجی با برچسب «nPOC»ی است که قیمت از پایان آن پروفایل هنوز به آن برنگشته؛ با رسیدن قیمت، به‌صورت نقطه‌چین کم‌رنگ «touched» علامت می‌خورد.
  • ردیف‌های «POC migration» و «VA vs previous» جابه‌جایی POC و Value Area نسبت به پروفایل قبلی را نشان می‌دهند؛ «Rel. volume» حجم تا این لحظه را با میانگین همان بازه‌ی زمانی در پروفایل‌های قبلی مقایسه می‌کند.
  • ردیف «Note» وضعیت کلی را خلاصه می‌کند: OK یعنی داده سالم است؛ در غیر این صورت دلیل (حجم تیک، کندل بدون ریزکندل، تایم‌فریم چارت بزرگ‌تر از دوره و مانند آن) همان‌جا نوشته می‌شود.
  • این اندیکاتور سیگنال خرید/فروش نمی‌دهد؛ تفسیر سطوح با خود تحلیلگر است.

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

چرا روی بعضی تایم‌فریم‌ها اول چیزی نشان نمی‌دهد؟

اگر «Profile period» را دستی روی Day/Week گذاشته باشید و خودِ چارت هم همان تایم‌فریم یا بالاتر باشد، کندل ریزتری داخل دوره وجود ندارد و اندیکاتور منتظر می‌ماند (در جدول «Chart TF ≥ period» دیده می‌شود). با گزینه‌ی پیش‌فرض Auto این اتفاق نمی‌افتد.

فرق حجم Real و Tick چیست؟

در ارزهای دیجیتال حجم واقعی صرافی (Real) در دسترس است. در فارگس و CFD بیشتر بروکرها فقط حجم تیک (تعداد تغییر قیمت) می‌دهند که تقریبی از گردش واقعی سرمایه است، نه خودش؛ در جدول و با رنگ نارنجی مشخص می‌شود.

چرا یک پروفایل در جدول «داده‌ی ناقص» یا نارنجی علامت خورده؟

یعنی برای بخشی از آن دوره ریزکندل در دسترس نبوده و به حجم کندل چارت (fallback) برگشته، یا کندل مرزی دوره کامل نبوده. چنین پروفایلی برای هشدارهای سطح هم به‌صورت پیش‌فرض نادیده گرفته می‌شود تا هشدار اشتباه ندهد.

پروفایل ترکیبی (Composite) و لنگرشده (Anchored) چه فرقی دارند؟

Composite چند پروفایل تمام‌شده (مثلاً ۵ روز آخر) را روی یک شبکه‌ی قیمتی مشترک جمع می‌زند بدون شمارش دوباره‌ی حجم. Anchored یک پروفایل تکی از یک تاریخ/ساعت دلخواه تا الان می‌سازد (در TradingView از کندل‌های چارت، در متاتریدر از ریزکندل).

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

بله، MT5 و MT4 با همان ریاضیات POC/Value Area/HVN-LVN/Naked POC و همان حالت Auto برای انتخاب دوره. جدول وضعیت در متاتریدر پیش‌فرض خاموش است؛ از تنظیمات اندیکاتور روشن می‌شود.

چارت شلوغ یا کند شده، چه کنم؟

لایه‌های اختیاری (Initial Balance، ترکیبی، لنگرشده، چندسشن، Confluence، گره‌های HVN/LVN) را یکی‌یکی خاموش کنید و «Previous profiles to show» را کم کنید.

اندیکاتور همراه: اگر سبک اسمارت مانی هم می‌خواهید: Crypex SMC (ساختار بازار، اوردر بلاک، FVG و نقدینگی)

می‌خواهید بدانید این استراتژی را کجا اجرا کنید؟ صرافی‌های ارز دیجیتال و بروکرهای فارکس را مقایسه کنید.

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

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