市场轮廓指标 (第二部分):基于画布的优化与渲染·综合运用
📘

市场轮廓指标 (第二部分):基于画布的优化与渲染·综合运用

第 3/3 篇

「按日重建市场轮廓的循环逻辑」

指标在 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 的概率。

MQL5 / C++
      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。

MQL5 / C++
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,调用方据此判断是要新建剖面还是复用旧实例。外汇与贵金属行情跳空频繁,这种按日期索引的复用机制能显著降低重绘开销,但高频切换品种仍属高风险操作,实盘前请在模拟盘验证。

MQL5 / C++
   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 加载后改改会话配色和默认日数,比读十遍文档来得实在。

常见问题

在指标初始化时按当前交易日清空旧对象并重新拉取当日 tick 数据重绘,用日期字符串做键值就能只重建当天剖面。
挂好生命周期钩子,在 ChartEvent 里捕获重绘与周期变更事件,只更新受影响的坐标系而不全量删对象。
可以,小布能按品种和交易日直接调出已存的剖面对象并标出价值区,你只需打开对应页面确认。
多半是对象名里日期格式和时区不一致,统一用经纪商服务器时间的 YYYYMMDD 做后缀再比对即可。
不是,控制价只代表成交最密集区,价格触及后可能停留也可能突破,应结合后续分布宽度判断概率。