市场轮廓指标 (第二部分):基于画布的优化与渲染·综合运用
「按日重建市场轮廓的循环逻辑」
指标在 OnCalculate 里用 for 循环遍历从 InpStartDate 到 lastday 的每一天。首次运行 lastday 取 InpStartDate+InpShowDays,之后每次 tick 只算 InpStartDate+1,也就是仅处理最新一天,历史日不再重算,省 CPU。 循环内先声明 MqlRates day_rate[],调用 CopyRates(Symbol(), PERIOD_D1, day, 1, day_rate) 取当日日线数据;若返回 -1(周末/假期无 tick 或日线未就绪),直接 return(prev_calculated) 挂起等下一轮。拿到后由 day_rate[0].time 算出当日起止:stop_time = start_time + PeriodSeconds(PERIOD_D1) - 1。 接着用 CopyRates(Symbol(), PERIOD_CURRENT, start_time, stop_time, bars_in_day) 抓当日所有 intraday 棒。若 prev_calculated>0,就通过 GetMarketProfileByDate 按开盘时间找已有 CMarketProfile 对象;找不到则返回 0 强制全量重算并打印日志。找到后调用 SetHiLoBars 把日高日低和 intraday 数组灌进去,对象会自动移到新日高坐标并重初始化向量。 首算分支走 else:直接 new CMarketProfile(ExtPrefixUniq, start_time, stop_time, day_rate[0].high, day_rate[0].low, bars_in_day) 建新对象。外汇与贵金属市场跳空频繁,周末 CopyRates 失败属常态,MT5 上开日线图预加载数据能降低返回 -1 的概率。
class="kw">return(rates_total); } class=class="str">"cmt">//--- update opening time open_time=current_open; class=class="str">"cmt">//--- we will only calculate one day from now on, since all other days have already been calculated during the first run lastday=InpStartDate+class="num">1; } class=class="str">"cmt">//--- in a loop for the specified number of days(either InpStartDate+InpShowDays on first run, or InpStartDate+class="num">1 on each tick) for(class="type">uint day=InpStartDate; day<lastday; day++) { class=class="str">"cmt">//--- get the data of the day with index day into the structure class="type">MqlRates day_rate[]; class=class="str">"cmt">//--- if the indicator is launched on weekends or holidays when there are no ticks, you should first open the daily chart of the symbol class=class="str">"cmt">//--- if we have not received bar data for the day index of the daily period, we leave until the next call to OnCalculate() if(CopyRates(Symbol(), PERIOD_D1, day, class="num">1, day_rate)==-class="num">1) class="kw">return(prev_calculated); class=class="str">"cmt">//--- get day start and end time class="type">class="kw">datetime start_time=day_rate[class="num">0].time; class="type">class="kw">datetime stop_time=start_time+PeriodSeconds(PERIOD_D1)-class="num">1; class=class="str">"cmt">//--- get all intraday bars of the current day class="type">MqlRates bars_in_day[]; if(CopyRates(Symbol(), PERIOD_CURRENT, start_time, stop_time, bars_in_day)==-class="num">1) class="kw">return(prev_calculated); CMarketProfile *market_profile; class=class="str">"cmt">//--- if the Market Profile has already been created and its drawing has been performed earlier if(prev_calculated>class="num">0) { class=class="str">"cmt">//--- find the Market Profile object(CMarketProfile class) in the list by the opening time of the day with the &class="macro">#x27;day&class="macro">#x27; index market_profile=GetMarketProfileByDate(ExtPrefixUniq, start_time); class=class="str">"cmt">//--- if the object is not found, class="kw">return zero to completely recalculate the indicator if(market_profile==NULL) { PrintFormat("Market Profile not found for %s. Indicator will be recalculated for all specified days", TimeToString(start_time, TIME_DATE)); class="kw">return(class="num">0); } class=class="str">"cmt">//--- CMarketProfile object is found in the list; set it to High and Low values of the day and pass the array of intraday bars class=class="str">"cmt">//--- in this case, the object is shifted to a new coordinate corresponding to the High of the daily candle, and all arrays(vectors) are reinitialized market_profile.SetHiLoBars(day_rate[class="num">0].high, day_rate[class="num">0].low, bars_in_day); } class=class="str">"cmt">//--- if this is the first calculation else { class=class="str">"cmt">//--- create a new object of the CMarketProfile class to store the Market Profile of the day with &class="macro">#x27;day&class="macro">#x27; index market_profile = new CMarketProfile(ExtPrefixUniq, start_time, stop_time, day_rate[class="num">0].high, day_rate[class="num">0].low, bars_in_day);
画布指标的生命周期钩子
市场轮廓画布指标在 OnCalculate 收尾阶段,先把每个交易时段的轮廓对象算完再统一重绘。循环结束后调用 ChartRedraw(0) 强制刷新主图表,并返回 rates_total 作为下一轮 OnCalculate 的基准 BAR 数,避免重复计算已处理的历史 K 线。 退出时 OnDeinit 负责清理:用 mp_list.Count() 拿到对象总数,逐个 TryGetValue 取指针,确认 CheckPointer 不等于 POINTER_INVALID 才 delete。日志会打印带 ExtPrefixUniq 前缀的删除提示,方便在 Experts 标签里核对有没有漏删对象。 图表缩放或拖动会触发 CHARTEVENT_CHART_CHANGE,此时不重算轮廓,只遍历列表对 isVisibleOnChart() 为真的对象刷新尺寸。外汇与贵金属品种在切换周期时事件频发,若发现轮廓闪退,优先查这个分支是否漏了 ChartRedraw。
class=class="str">"cmt">//--- redraw the chart after the loop has been completed and all objects have been created and updated ChartRedraw(class="num">0); class=class="str">"cmt">//--- class="kw">return the number of bars for the next OnCalculate call class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- class="kw">delete all Market Profile graphical objects after use Print("Indicator \"Market Profile Canvas\" stopped, class="kw">delete all objects CMarketProfile with prefix=", ExtPrefixUniq); class=class="str">"cmt">//--- in a loop by the number of CMarketProfile objects in the list class="type">int size=mp_list.Count(); for(class="type">int i=class="num">0; i<size; i++) { class=class="str">"cmt">//--- get the pointer to the CMarketProfile object from the list by the loop index CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); class=class="str">"cmt">//--- if the pointer is valid and the object exists, class="kw">delete it if(market_profile!=NULL) if(CheckPointer(market_profile)!=POINTER_INVALID) class="kw">delete market_profile; } class=class="str">"cmt">//--- redraw the chart to display the result immediately ChartRedraw(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator chart&class="macro">#x27;s event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam) { class=class="str">"cmt">//--- if this is a user event, leave if(id>=CHARTEVENT_CUSTOM) class="kw">return; class=class="str">"cmt">//--- if there is a chart change, update the sizes of all objects of the CMarketProfile class with redrawing the chart if(CHARTEVENT_CHART_CHANGE==id) { class=class="str">"cmt">//--- in a loop by the number of CMarketProfile objects in the list class="type">int size=mp_list.Count(); for(class="type">int i=class="num">0; i<size; i++) { class=class="str">"cmt">//--- get the pointer to the CMarketProfile object by the loop index CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); class=class="str">"cmt">//--- if the object is received and if it is in the visible area of the chart if(market_profile) if(market_profile.isVisibleOnChart()) {
◍ 按日期捞回已有的剖面对象
在 MT5 里维护多日市场剖面(Market Profile)时,最忌每次刷新都重建对象。上面这段逻辑先在循环里通过 mp_list.Count() 拿到当前列表长度,再逐个 TryGetValue 取出 CMarketProfile 指针,避免重复 new 带来的内存抖动。 取指针后必须用 CheckPointer(market_profile)!=POINTER_INVALID 确认不是悬空引用,否则在实时行情回调里极易触发崩溃。确认有效后,用 market_profile.Check(prefix, time) 比对前缀与时间戳,命中就直接 return 该指针,没命中继续找。 整段找不到匹配项时统一返回 NULL,调用方据此判断是要新建剖面还是复用旧实例。外汇与贵金属行情跳空频繁,这种按日期索引的复用机制能显著降低重绘开销,但高频切换品种仍属高风险操作,实盘前请在模拟盘验证。
class=class="str">"cmt">//--- update canvas dimensions and redraw market profile histograms market_profile.UpdateSizes(); market_profile.Draw(InpMultiplier); } } class=class="str">"cmt">//--- update the chart after recalculating all Profiles ChartRedraw(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Returns CMarketProfile or NULL by the date | class=class="str">"cmt">//+------------------------------------------------------------------+ CMarketProfile* GetMarketProfileByDate(class="type">class="kw">string prefix, class="type">class="kw">datetime time) { class=class="str">"cmt">//--- in a loop by the number of CMarketProfile objects in the list class="type">int size=mp_list.Count(); for(class="type">int i=class="num">0; i<size; i++) { class=class="str">"cmt">//--- get the pointer to the CMarketProfile object by the loop index CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); class=class="str">"cmt">//--- if the pointer is valid and the object exists, if(market_profile!=NULL) if(CheckPointer(market_profile)!=POINTER_INVALID) { class=class="str">"cmt">//--- if the Market Profile object obtained by the pointer was created for the required time, class="kw">return the pointer if(market_profile.Check(prefix, time)) class="kw">return(market_profile); } } class=class="str">"cmt">//--- nothing found - class="kw">return NULL class="kw">return(NULL); }
「把这条线请下神坛」
把数千个图形对象压成单个 OBJ_BITMAP 画布后,默认 7 个交易日的轮廓只占用 3 个位图对象,资源开销从量级上被砍掉。但即时重渲染时图像仍会出现可见闪烁与抖动,说明渲染触发逻辑还没收敛到位。 CMarketProfile 里那个没实现的 isChartScaleChanged() 才是关键——只在图表刻度真正变动时才重绘,能消掉大部分无用刷新。外汇与贵金属市场高杠杆、滑点无常,这类视觉优化只解决卡顿,不替你过滤假突破。 代码永远有进一步榨取的空间,换一套视觉构建概念往往比原地调参更有效。附带的 MarketProfile_Canvas.mq5 带完整注释,开 MT5 加载后改改会话配色和默认日数,比读十遍文档来得实在。