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


این تصویرها اسکرینشات از TradingView یا متاتریدر نیستند؛ با اجرای همین کد روی دادهی واقعی بازار ساخته و رسم شدهاند. ظاهر دقیق در پلتفرم شما (فونت، رنگها و اندازه) کمی متفاوت است.
// 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)
//+------------------------------------------------------------------+
//| 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;
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| 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 و نقدینگی)
میخواهید بدانید این استراتژی را کجا اجرا کنید؟ صرافیهای ارز دیجیتال و بروکرهای فارکس را مقایسه کنید.
این کد آموزشی است و توصیهی سرمایهگذاری نیست. قبل از استفاده روی حساب واقعی، کامپایل و رفتار آن را روی حساب دمو و در بکتست بررسی کنید؛ نتیجهی گذشته تضمینکنندهی آینده نیست.
← همهی اسکریپتها