市场轮廓指标 (第二部分):基于画布的优化与渲染·进阶篇
「可见性与分时段计数的底层判定」
市场轮廓图要画得不出界,先得确认画布时间窗落在图表可视范围内。isVisibleOnChart 用 CHART_FIRST_VISIBLE_BAR 和 CHART_VISIBLE_BARS 反推左右边界的 bar 索引,再拿 iTime 取对应 datetime,只要轮廓的起止时间 m_time1 / m_time2 任一落入该区间就返回 true。 分时段统计是轮廓厚度的来源。CalculateSessions 先取 SYMBOL_POINT,若 m_bars 为空直接退出;随后遍历当日每根 bar,用 TimeToStruct 拿小时数,按 (low-m_low)/point 与 (high-m_low)/point 算出自日低起算的价位格子索引。 小时数大于等于 InpAmericaStartHour 的 bar 被归入美盘,循环里把 m_america 对应价位区间的计数逐个加一;其余归欧盘。这种按 point 离散化价位的做法,意味着点值越小格子越密,XAUUSD 这类点值 0.01 的品种会比 EURUSD 更吃内存。 外汇与贵金属杠杆高、跳空频繁,m_low 若取错日低会导致整列索引偏移,开 MT5 跑前务必核对 m_low 的赋值来源。
m_canvas.Resize(m_width, m_height); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Checks that the profile is in the visible part of the chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMarketProfile::isVisibleOnChart(class="type">void) { class="type">long last_bar=ChartGetInteger(class="num">0, CHART_FIRST_VISIBLE_BAR); class=class="str">"cmt">// last visible bar on the chart on the left class="type">long first_bar=last_bar+-ChartGetInteger(class="num">0, CHART_VISIBLE_BARS); class=class="str">"cmt">// first visible bar on the chart on the right first_bar=first_bar>class="num">0?first_bar:class="num">0; class="type">class="kw">datetime left =iTime(Symbol(), Period(), (class="type">int)last_bar); class=class="str">"cmt">// time of the left visible bar on the chart class="type">class="kw">datetime right=iTime(Symbol(), Period(), (class="type">int)first_bar); class=class="str">"cmt">// time of the right visible bar on the chart class=class="str">"cmt">//--- class="kw">return a flag that the canvas is located inside the left and right visible bars of the chart class="kw">return((m_time1>= left && m_time1 <=right) || (m_time2>= left && m_time2 <=right)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Prepares profile arrays by sessions | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMarketProfile::CalculateSessions(class="type">void) { class="type">class="kw">double point=SymbolInfoDouble(Symbol(), SYMBOL_POINT); class=class="str">"cmt">// one point value class=class="str">"cmt">//--- if the array of intraday bars is not filled, leave if(ArraySize(m_bars)==class="num">0) class="kw">return(class="kw">false); class=class="str">"cmt">//---- iterate over all the bars of the current day and mark the cells of the arrays(vectors) that contain the bars being iterated over in the loop class="type">int size=ArraySize(m_bars); for(class="type">int i=class="num">0; i<size; i++) { class=class="str">"cmt">//--- get the bar hour class="type">MqlDateTime bar_time; TimeToStruct(m_bars[i].time, bar_time); class="type">uint hour =bar_time.hour; class=class="str">"cmt">//--- calculate price levels in points from the Low of the day reached by the price on each bar of the loop class="type">int start_box=(class="type">int)((m_bars[i].low-m_low)/point); class=class="str">"cmt">// index of the beginning of price levels reached by the price on the bar class="type">int stop_box =(class="type">int)((m_bars[i].high-m_low)/point); class=class="str">"cmt">// index of the end of price levels reached by the price on the bar class=class="str">"cmt">//--- American session if(hour>=InpAmericaStartHour) { class=class="str">"cmt">//--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level for(class="type">int ind=start_box; ind<stop_box; ind++) m_america[ind]++; } else { class=class="str">"cmt">//--- European session
◍ 把三个会话的影子叠到一块画布上
欧盘和美盘的时间窗用 hour 判断切分:当 hour 落在 InpEuropeStartHour 与 InpAmericaStartHour 之间,就给 m_europe 对应价位区间的计数加一;否则归入 m_asia。这样亚、欧两盘的停留频次各自独立累计,后续合成时不会互相污染。 绘图阶段先向量相加:total_profile = m_asia + m_europe + m_america 得到全时段轮廓,europe_asia = m_asia + m_europe 只取前两盘。画布用 ColorToARGB(clrBlack, 0) 擦成全透明,避免遮挡主图。 美盘矩形从 x1=0 起笔,y1 由 m_height - int(i*m_vert_scale) 算左下角,y2 再叠一个垂直刻度;x2 宽度 = total_profile[i]*m_hor_scale*multiplier。跳过 total_profile[i]==0 的空格,只对有过报价的位置 FillRectangle,透明度由 InpTransparency 控制。 想验证配色逻辑,把 InpAmericaSession 和 InpTransparency 调一下,重跑 Draw(),MT5 子图里美盘色块会立刻变浓或变淡,欧盘循环同理用 europe_asia 向量喂进去即可。外汇与贵金属行情跳空频繁,这种轮廓仅反映历史停留概率,实盘须自担高风险。
if(hour>=InpEuropeStartHour && hour<InpAmericaStartHour) class=class="str">"cmt">//--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level for(class="type">int ind=start_box; ind<stop_box; ind++) m_europe[ind]++; class=class="str">"cmt">//--- Asian session else class=class="str">"cmt">//--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level for(class="type">int ind=start_box; ind<stop_box; ind++) m_asia[ind]++; } } class=class="str">"cmt">//--- vectors of all sessions are ready class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Draw Market Profile on the canvas | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMarketProfile::Draw(class="type">class="kw">double multiplier=class="num">1.0) { class=class="str">"cmt">//--- sum up all sessions for rendering vector total_profile=m_asia+m_europe+m_america; class=class="str">"cmt">// profile that combines all sessions vector europe_asia=m_asia+m_europe; class=class="str">"cmt">// profile that combines only the European and Asian sessions class=class="str">"cmt">//--- set a completely transparent background for the canvas m_canvas.Erase(ColorToARGB(clrBlack, class="num">0)); class=class="str">"cmt">//--- variables for drawing rectangles class="type">int x1=class="num">0; class=class="str">"cmt">// X coordinate of the left corner of the rectangle always starts at zero class="type">int y1, x2, y2; class=class="str">"cmt">// rectangle coordinates class="type">int size=(class="type">int)total_profile.Size(); class=class="str">"cmt">// size of all sessions class=class="str">"cmt">//--- render the American session with filled rectangles for(class="type">int i=class="num">0; i<size; i++) { class=class="str">"cmt">//--- skip zero vector values if(total_profile[i]==class="num">0) class="kw">continue; class=class="str">"cmt">//--- calculate two points to draw a rectangle, x1 is always class="num">0 (X of the lower left corner of the rectangle) y1=m_height-class="type">int(i*m_vert_scale); class=class="str">"cmt">// Y coordinate of the lower left corner of the rectangle y2=(class="type">int)(y1+m_vert_scale); class=class="str">"cmt">// Y coordinate of the upper right corner of the rectangle x2=(class="type">int)(total_profile[i]*m_hor_scale*multiplier); class=class="str">"cmt">// X coordinate of the upper right corner of the rectangle class=class="str">"cmt">//--- draw a rectangle at the calculated coordinates with the class="type">color and transparency set for the American session m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAmericaSession, InpTransparency)); } class=class="str">"cmt">//--- render the European session with filled rectangles for(class="type">int i=class="num">0; i<size; i++) { class=class="str">"cmt">//--- skip zero vector values
三时段轮廓的矩形叠加与合并向量
把欧盘与亚盘合并轮廓用矩形画到画布上,逻辑和美股时段一致:先跳过 total_profile 里为 0 的价位档,再按纵向比例算 y1、y2,横向宽度由 europe_asia[i] 乘水平比例和倍数得到。 代码里 europe_asia 实际是 m_asia+m_europe,注释写成了欧盘,这是个容易看走眼的点。FillRectangle 用 InpEuropeSession 配透明度上色,画的是亚欧叠加块,不是美盘。 亚盘单独渲染时换用 m_asia[i] 算 x2,颜色走 InpAsiaSession。两段循环结构相同,只是数据源和配色参数分开,方便交易者调透明度看重叠密度。 合并所有时段只需一句向量加法:total_profile=m_asia+m_europe+m_america。示意数据 9.00000 12.00000 15.00000 表示三个价位档的合计成交量,验证时可在 MT5 脚本里 Print(total_profile) 看实际输出。 析构里 ObjectsDeleteAll(0,m_prefix,0,OBJ_BITMAP) 清掉前缀位图对象,ChartRedraw 重绘。开 MT5 挂上这类指标,改 InpTransparency 从 50 调到 20,重叠区颜色会明显变深,外汇与贵金属市场波动剧烈,此类可视化仅辅助判断密度分布,不预示方向。
if(total_profile[i]==class="num">0) class="kw">continue; class=class="str">"cmt">//--- calculate two points to draw a rectangle y1=m_height-class="type">int(i*m_vert_scale); y2=(class="type">int)(y1+m_vert_scale); x2=(class="type">int)(europe_asia[i]*m_hor_scale*multiplier); class=class="str">"cmt">//--- draw a rectangle over the rendered American session class="kw">using the calculated coordinates class=class="str">"cmt">//--- with class="type">color and transparency set for the European session m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpEuropeSession, InpTransparency)); } class=class="str">"cmt">//--- draw the Asian session with filled rectangles for(class="type">int i=class="num">0; i<size; i++) { class=class="str">"cmt">//--- skip zero vector values if(total_profile[i]==class="num">0) class="kw">continue; class=class="str">"cmt">//--- calculate two points to draw a rectangle y1=m_height-class="type">int(i*m_vert_scale); y2=(class="type">int)(y1+m_vert_scale); x2=(class="type">int)(m_asia[i]*m_hor_scale*multiplier); class=class="str">"cmt">//--- draw a rectangle over the rendered European session class="kw">using the calculated coordinates class=class="str">"cmt">//--- with class="type">color and transparency set for the Asian session m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAsiaSession, InpTransparency)); } class=class="str">"cmt">//--- update the OBJ_BITMAP object without redrawing the chart m_canvas.Update(class="kw">false); } class=class="str">"cmt">//--- sum up all sessions for rendering vector total_profile=m_asia+m_europe+m_america; class=class="str">"cmt">// profile that combines all sessions vector europe_asia=m_asia+m_europe; class=class="str">"cmt">// profile that combines only the European and Asian sessions vector total_profile=m_asia+m_europe+m_america; class=class="str">"cmt">// 结合了所有时段的轮廓 class="type">void CMarketProfile::~CMarketProfile(class="type">void) { class=class="str">"cmt">//--- class="kw">delete all graphical objects after use ObjectsDeleteAll(class="num">0, m_prefix, class="num">0, OBJ_BITMAP); ChartRedraw(); }
「用泛型列表和画布重写市场轮廓指标」
打开 MT5 安装目录下的 \MQL5\Indicators\Free Indicators\MarketProfile Canvas.mq5,这套实现把市场轮廓的绘制从传统对象改成 CCanvas 渲染 + CArrayList<T> 泛型指针列表。头部先 include 两个类文件:Canvas\Canvas.mqh 负责简化自定义绘图,Generic\ArrayList.mqh 提供泛型列表容器。 输入参数里 InpShowDays 默认 7,代表从 InpStartDate(默认 0 即当天)起往回含当天共展示 7 个交易日的轮廓;InpTransparency 默认 150(0 为全透明),三个时段颜色分别用 clrGold / clrBlue / clrViolet 标记亚欧美盘。 因为 CMarketProfile 类定义在指标代码下方,所以文件前半部分必须做类前置声明,否则编译报“未知变量类型”。那个 CArrayList<CMarketProfile*> 列表专门存指向各日轮廓对象的指针,循环里按天取对象,没有就 new 一个塞进去。 OnInit 用系统启动后毫秒数末 4 位拼图形对象前缀,避免多图表实例冲突。OnCalculate 的核心循环逻辑:按显示天数遍历 -> 取该日结构 -> 算当日包含当前周期 K 线数 -> 从列表拿或新建轮廓对象 -> 以像素测最低到最高波幅并重置时段向量 -> 按新 K 线尺寸调画布 -> 重算并重绘每个时段轮廓,循环结束调一次图表重绘。 图表缩放会触发 OnChartEvent,此时所有 CMarketProfile 对象要按新刻度重算尺寸并重绘,否则直方图比例会歪。返回指定日开盘时间对应轮廓对象的函数,让指标能按时间精准拿到指针做后续更新,不用全量重建。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MarketProfile Canvas.mq5 | class=class="str">"cmt">//| Copyright class="num">2009-class="num">2024, MetaQuotes Ltd | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2022, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_plots class="num">0 class="macro">#include <Canvas\Canvas.mqh> class="macro">#include <Generic\ArrayList.mqh> class=class="str">"cmt">//--- input parameters class=class="str">"cmt">//--- input parameters input class="type">uint InpStartDate =class="num">0; class=class="str">"cmt">/* day number to start calculation */ class=class="str">"cmt">// class="num">0 - current, class="num">1 - previous, etc. input class="type">uint InpShowDays =class="num">7; class=class="str">"cmt">/* number of days to display */ class=class="str">"cmt">// starting with and including the day in InpStartDate input class="type">int InpMultiplier =class="num">1; class=class="str">"cmt">/* histogram length multiplier */ input class="type">color InpAsiaSession =clrGold; class=class="str">"cmt">/* Asian session */ input class="type">color InpEuropeSession =clrBlue; class=class="str">"cmt">/* European session */ input class="type">color InpAmericaSession =clrViolet; class=class="str">"cmt">/* American session */ input class="type">uchar InpTransparency =class="num">150; class=class="str">"cmt">/* Transparency, class="num">0 = invisible */ class=class="str">"cmt">// market profile transparency, class="num">0 = fully transparent input class="type">uint InpEuropeStartHour=class="num">8; class=class="str">"cmt">/* European session opening hour */
◍ 美盘时段画布对象的初始化坑点
这段代码片段来自一个市场轮廓(Market Profile)自定义指标,目的是在美盘开盘(默认 14 点)后生成独立前缀的图形对象,避免多实例冲突。注意输入参数 InpAmericaStartHour=14 直接锁定了美盘开始的小时数,外汇与贵金属交易者改这个数值就能切换统计起点,但这类品种杠杆高、滑点大,参数误用可能放大回测与实盘偏差。
代码里先声明 ExtPrefixUniq 字符串存唯一前缀,又在后面写了 class CMarketProfile; 的前向声明和 CArrayList<CMarketProfile*> mp_list; 指针列表。编译时若报「‘CMarketProfile’ - 意外的标记」,通常是因为类的前向声明和模板列表写在了函数外却没包含完整头文件或类定义顺序错乱,MT5 编辑器会直接卡在语法阶段。
OnInit() 中用 GetTickCount64() 取系统计时拼成 4 位后缀做前缀,例如某次运行 Print 输出 prefix=4821,保证同图表加载多个副本时对象名不撞车。OnCalculate() 则通过 iTime(Symbol(), PERIOD_D1, 0) 抓日线开盘时间,并用 InpStartDate+InpShowDays 推算最后统计日,若 prev_calculated!=0 且 open_time==current_open 则跳过当日重算。
开 MT5 把这段贴进新指标工程,故意把 CMarketProfile 类定义放列表之后,就能复现那个“意外的标记”报错;调一下 InpAmericaStartHour 到 8 或 20,观察前缀逻辑是否仍按日线开口刷新,比看文档直观。
input class="type">uint InpAmericaStartHour=class="num">14; class=class="str">"cmt">/* American session opening hour */ class=class="str">"cmt">//--- unique prefix to identify graphical objects belonging to the indicator class="type">class="kw">string ExtPrefixUniq; class=class="str">"cmt">//--- declare CMarketProfile class class CMarketProfile; class=class="str">"cmt">//--- declare a list of pointers to objects of the CMarketProfile class CArrayList<CMarketProfile*> mp_list; ‘CMarketProfile’ - 意外的标记 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create a prefix for object names class="type">class="kw">string number=StringFormat("%I64d", GetTickCount64()); ExtPrefixUniq=StringSubstr(number, StringLen(number)-class="num">4); Print("Indicator \"Market Profile Canvas\" started, prefix=", ExtPrefixUniq); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator iteration function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class=class="str">"cmt">//--- opening time of the current daily bar class="type">class="kw">datetime class="kw">static open_time=class="num">0; class=class="str">"cmt">//--- number of the last day for calculations class=class="str">"cmt">//--- (if InpStartDate = class="num">0 and InpShowDays = class="num">3, lastday = class="num">3) class=class="str">"cmt">//--- (if InpStartDate = class="num">1 and InpShowDays = class="num">3, lastday = class="num">4) etc ... class="type">uint lastday=InpStartDate+InpShowDays; class=class="str">"cmt">//--- if the first calculation has already been made if(prev_calculated!=class="num">0) { class=class="str">"cmt">//--- get the opening time of the current daily bar class="type">class="kw">datetime current_open=iTime(Symbol(), PERIOD_D1, class="num">0); class=class="str">"cmt">//--- if we do not calculate the current day if(InpStartDate!=class="num">0) { class=class="str">"cmt">//--- if the opening time was not received, leave if(open_time==current_open)