价格直方图(市场概况)及其在 MQL5 中的实施·进阶篇
(2/3)· 钟形曲线背后是拍卖逻辑,本篇拆解 MQL5 如何把价格与时间关系落成可运行的价格直方图
把价格直方图做成无交易EA而非指标
早期在 MT4 用 MQL4 写的价格直方图碰过不少坑:M1、M5 历史柱编号太短,假日与 CFD schedule 要自己写函数处理,切周期还会重复计算拖慢终端。MT5 测试版出来后,我直接转去 MQL5 重写。 指标方案先被否决。OnCalculate() 跑 260 天分钟数据要数分钟,而切时间表时系统会销毁旧指标、新建并重算,同样的活重复花大量时间。MQL5 文档里藏着解法:把它写成不交易的 EA。 EA 的好处很实在:OnTick() 里的 Init 不卡运行时;DeInit 带 reason 参数,切表只发 REASON_CHARTCHANGE,不卸载 EA 和全局变量。这样附加后一次性算完,新交易日进数据再增量更新。 源码拆成 4 个文件,主文件 PriceHistogram.mq5 同目录放 \MQL5\Experts\PriceHistogram,其余 .mqh 是类描述。下面这段是主文件开头,输入参数决定直方图天数与回看长度。 类设计上,CExpert 管事件,CPriceHistogram 管单日直方图,CProgressBar 管进度条——因为 500 天分钟历史处理久,必须可视化免得以为终端死了。
class="macro">#include "ClassExpert.mqh" class=class="str">"cmt">// 输入参数区块 class="kw">input class="type">int DayTheHistogram = class="num">10; class=class="str">"cmt">// 直方图天数 class="kw">input class="type">int DaysForCalculation= class="num">500; class=class="str">"cmt">// 用于计算的天数(-1代表所有)
「初始化时先卡住行情同步再灌参数」
MT5 自定义指标或 EA 在 OnInit 里最容易翻车的不是画图,而是历史数据还没同步完就开算。上面这段逻辑在正式计算前用 while 循环去查 SERIES_SYNCHRONIZED,每次睡 500 毫秒、最多试 AMOUNT_OF_ATTEMPTS 次,外汇和贵金属品种在切换周期时尤其容易卡这一步,不处理就可能拿到半截日线。 RangePercent 默认给 70,代表内部范围取成交量分布里占比 70% 的价格带;InnerRange 填 Indigo、OuterRange 填 Magenta、ControlPoint 填 Orange,分别对应范围内、范围外和 POC 控制点的颜色。ShowValue 置 true 时会在图上直接标出数值。 这些 input 在 OnInit 里被逐一赋给 ExtExpert 实例,再调 ExtExpert.Init() 完成 CExpert 类启动。DaysForCalculation 设 -1 表示吃全部历史日线,DayTheHistogram 控制直方图对应的具体交易日,参数不对,后面分布图就会偏。 [CODE] 里 CExpert 类用 CList 挂了一串 CObject 派生对象,name_symbol 存品种名、count_bars 存日线根数、event_on 管事件开关。想验证就开 MT5 把 RangePercent 改成 50 或 90,看内部范围收缩或扩张是否符合预期,贵金属跳空日可能让分布断层,属正常。
class="kw">input class="type">uint RangePercent = class="num">70; class=class="str">"cmt">// 百分比范围 class="kw">input class="type">class="kw">color InnerRange =Indigo; class=class="str">"cmt">// 内部范围 class="kw">input class="type">class="kw">color OuterRange =Magenta; class=class="str">"cmt">// 外部范围 class="kw">input class="type">class="kw">color ControlPoint =Orange; class=class="str">"cmt">// 控制点 class="kw">input class="type">bool ShowValue =true; class=class="str">"cmt">// 显示值 class="type">int OnInit() { class=class="str">"cmt">//--- class=class="str">"cmt">// 在开始计算前,检查交易品种的同步情况 class="type">int err=class="num">0; while(!(class="type">bool)SeriesInfoInteger(Symbol(),class="num">0,SERIES_SYNCRONIZED) && err<AMOUNT_OF_ATTEMPTS) { Sleep(class="num">500); err++; } class=class="str">"cmt">// CExpert 类的初始化 ExtExpert.RangePercent=RangePercent; ExtExpert.InnerRange=InnerRange; ExtExpert.OuterRange=OuterRange; ExtExpert.ControlPoint=ControlPoint; ExtExpert.ShowValue=ShowValue; ExtExpert.DaysForCalculation=DaysForCalculation; ExtExpert.DayTheHistogram=DayTheHistogram; ExtExpert.Init(); class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CExpert类 | class=class="str">"cmt">//| 类的描述 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CExpert { class="kw">public: class="type">int DaysForCalculation; class=class="str">"cmt">// 计算的天数(-1代表所有) class="type">int DayTheHistogram; class=class="str">"cmt">// 直方图的天数 class="type">int RangePercent; class=class="str">"cmt">// 百分比范围 class="type">class="kw">color InnerRange; class=class="str">"cmt">// 范围内的颜色 class="type">class="kw">color OuterRange; class=class="str">"cmt">// 范围外的颜色 class="type">class="kw">color ControlPoint; class=class="str">"cmt">// 控制点(POC)的颜色 class="type">bool ShowValue; class=class="str">"cmt">// 显示值 class="kw">private: CList list_object; class=class="str">"cmt">// CObject类的动态列表 class="type">class="kw">string name_symbol; class=class="str">"cmt">// 交易品种名称 class="type">int count_bars; class=class="str">"cmt">// 日线数量 class="type">bool event_on; class=class="str">"cmt">// 事件处理标识 class="kw">public:
◍ EA 骨架与日线历史加载的实作细节
要在 MT5 里搭一个价格直方图 EA,先得把 CExpert 派生类的生命周期方法定清楚。构造函数留空,析构函数直接调 Deinit(REASON_CHARTCLOSE) 做资源回收;Init() 返回 bool 供外部判断启动是否成功,Deinit(const int reason) 接收退出原因,OnTick / OnEvent / OnTimer 则分别承接报价、图表交互和定时器事件。 日线历史不是一次就能拿稳的。下面这段 do-while 会用 Bars(NULL,PERIOD_D1) 算可用天數,若 DaysForCalculation+1 小于总 bars 就只取前者,否则取全量;随后 CopyTime 拉取日线开盘时间,失败就 Sleep(1) 重试,直到 rates_total>0 或 err 达到 AMOUNT_OF_ATTEMPTS。若重试耗尽仍拿不到,Print 报错并把 name_symbol 置 NULL 后返回 false。 拿到数据后建一个 CProgressBar 盯加载进度,Maximum 设为 rates_total。随后 for 循环里逐个 new CPriceHistogram,配置 ShowLevel、ColorPOCs、ColorInner、ColorOuter、RangePercent 等参数,再 Init 当根日线区间;成功就塞进 list_object,失败立刻 delete 防内存泄漏,同时 progress.Value(i) 推进进度条。 定时器靠 EventSetTimer(秒数) 起、EventKillTimer() 收,别漏了 Kill,否则 EA 卸載后定时器仍可能空跑。外汇与贵金属波动剧烈、杠杆风险高,这类历史加载逻辑在实盘前务必在策略测试器用真实点差验证。
class=class="str">"cmt">// 类的构造函数 CExpert(); class=class="str">"cmt">// 类的析构函数 ~CExpert(){Deinit(REASON_CHARTCLOSE);} class=class="str">"cmt">// 初始化方法 class="type">bool Init(); class=class="str">"cmt">// 反初始化方法 class="type">void Deinit(const class="type">int reason); class=class="str">"cmt">// OnTick方法 class="type">void OnTick(); class=class="str">"cmt">// OnChartEvent()事件处理方法 class="type">void OnEvent(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">// OnTimer() 事件处理方法 class="type">void OnTimer(); }; class="type">int err=class="num">0; do { class=class="str">"cmt">// 计算历史数据中的可用天数 count_bars=Bars(NULL,PERIOD_D1); if(DaysForCalculation+class="num">1<count_bars) count=DaysForCalculation+class="num">1; else count=count_bars; if(DaysForCalculation<=class="num">0) count=count_bars; rates_total=CopyTime(NULL,PERIOD_D1,class="num">0,count,day_time_open); Sleep(class="num">1); err++; } while(rates_total<=class="num">0 && err<AMOUNT_OF_ATTEMPTS); if(err>=AMOUNT_OF_ATTEMPTS) { Print("There is no accessible history PERIOD_D1"); name_symbol=NULL; class="kw">return(false); } class=class="str">"cmt">// 我们在图标上创建进度条,来观察加载进度 CProgressBar *progress=new CProgressBar; progress.Create(class="num">0,"Loading",class="num">0,class="num">150,class="num">20); progress.Text("Calculation:"); progress.Maximum=rates_total; class=class="str">"cmt">// 在本循环中,创建CPriceHistogram的对象并初始化 class=class="str">"cmt">// 以及添加到对象列表中 for(class="type">int i=class="num">0;i<rates_total;i++) { CPriceHistogram *hist_obj=new CPriceHistogram(); class=class="str">"cmt">// hist_obj.StepHistigram(step); class=class="str">"cmt">// 设置标志显示文本标签 hist_obj.ShowLevel(ShowValue); class=class="str">"cmt">// 设置POCs颜色 hist_obj.ColorPOCs(ControlPoint); class=class="str">"cmt">// 设置范围内的颜色 hist_obj.ColorInner(InnerRange); class=class="str">"cmt">// 设置范围外的颜色 hist_obj.ColorOuter(OuterRange); class=class="str">"cmt">// 设置范围百分比 hist_obj.RangePercent(RangePercent); class=class="str">"cmt">// hist_obj.ShowSecondaryPOCs((i>=rates_total-DayTheHistogram),PeriodSeconds(PERIOD_D1)); if(hist_obj.Init(day_time_open[i],day_time_open[i]+PeriodSeconds(PERIOD_D1),(i>=rates_total-DayTheHistogram))) list_object.Add(hist_obj); else class="kw">delete hist_obj; class=class="str">"cmt">// 如果有错误则删除对象 progress.Value(i); }; EventSetTimer(time in seconds); EventKillTimer();
用类把日内价格直方图管起来
在 MT5 里做价格分布分析,最忌讳把绘图逻辑和数值计算搅在一根脚本里。把单日直方图封装成一个 CPriceHistogram 类,后面要叠加多日、做回看扫描都只是调对象的事。 这个类从 CObject 继承,私有段先存了当天高低点 high_day / low_day,以及 Init_passed 这个初始化闸门——没过初始化就别想刷新 POC,能少踩很多空指针坑。 它用两个 CList 分别装内部行和外部行(ListHistogramInner / ListHistogramOuter),再把 POC 主线和次级 POC 线做成 CChartObjectTrend 指针。实盘里黄金 M15 一天能堆出 40~60 根内部行,列表结构比数组好扩。 对外暴露的接口很直白:Init() 吃开盘收盘时间和是否画直方图的开关;ShowLevel(bool) 控制 POC 水平线显隐,且只在 Init_passed 后调 RefreshPOCs();ShowHistogram() 同理管柱子本身。 次级 POC(second_poc_top / second_poc_bottom)和主 poc_value 都留了 double 字段,配合 range_percent 做百分比边界。外汇和贵金属波动大,这类分布工具只帮你看筹码重心,进场仍属高风险,别当信号神灯。
class CPriceHistogram : class="kw">public CObject { class="kw">private: class=class="str">"cmt">// 类的变量 class="type">class="kw">double high_day,low_day; class="type">bool Init_passed; class=class="str">"cmt">// 初始化是否通过的标识 CChartObjectTrend *POCLine; CChartObjectTrend *SecondTopPOCLine,*SecondBottomPOCLine; CChartObjectText *POCLable; CList ListHistogramInner; class=class="str">"cmt">// 存储内部行的列表 CList ListHistogramOuter;class=class="str">"cmt">// 存储外部行的列表 class="type">bool show_level; class=class="str">"cmt">// 显示水平的值 class="type">bool virgin; class=class="str">"cmt">// 原始的 class="type">bool show_second_poc; class=class="str">"cmt">// 显示次级POCs水平 class="type">class="kw">double second_poc_top; class=class="str">"cmt">// 顶部次级POC水平值 class="type">class="kw">double second_poc_bottom; class=class="str">"cmt">// 底部次级POC水平值 class="type">class="kw">double poc_value; class=class="str">"cmt">// POC水平值 class="type">class="kw">color poc_color; class=class="str">"cmt">// POC水平颜色 class="type">class="kw">datetime poc_start_time; class="type">class="kw">datetime poc_end_time; class="type">bool show_histogram; class=class="str">"cmt">// 显示直方图 class="type">class="kw">color inner_color; class=class="str">"cmt">// 直方图内部颜色 class="type">class="kw">color outer_color; class=class="str">"cmt">// 直方图外部颜色 class="type">uint range_percent; class=class="str">"cmt">// 百分比范围 class="type">class="kw">datetime time_start; class=class="str">"cmt">// 构建的开始时间 class="type">class="kw">datetime time_end; class=class="str">"cmt">// 构建的最后时间 class="kw">public: class=class="str">"cmt">// 类的构造函数 CPriceHistogram(); class=class="str">"cmt">// 类的析构函数 ~CPriceHistogram(){Delete();} class=class="str">"cmt">// 类的初始化 class="type">bool Init(class="type">class="kw">datetime time_open,class="type">class="kw">datetime time_close,class="type">bool showhistogram); class=class="str">"cmt">// 水平值 class="type">void ShowLevel(class="type">bool show){show_level=show; if(Init_passed) RefreshPOCs();} class="type">bool ShowLevel(){class="kw">return(show_level);} class=class="str">"cmt">// 显示直方图 class="type">void ShowHistogram(class="type">bool show); class="type">bool ShowHistogram(){class="kw">return(show_histogram);} class=class="str">"cmt">// 显示次级POC水平
「把 POC 控制接口和 M1 数据抓取拆开看」
这个类把 POC 的显示开关、颜色、百分比区间都做成了成对的 setter/getter。比如 ShowSecondaryPOCs(bool) 负责写开关并触发 RefreshPOCs(),无参版本则返回当前状态;ColorPOCs() 与 ColorInner()/ColorOuter() 同理,只是后两者在原文里函数体被略去,只留了声明。 RangePercent(uint) 值得单独说:它改的是 range_percent 成员,并且直接调 calculationPOCs() 重算,而不是只刷新画线。这意味着你动一次百分比参数,背后是整套直方图重新统计,不是简单挪根线。 计算核心落在 calculationPOCs() 里,它用 do 循环从 time_start 到 time_end 拉 M1 数据。CopyTime 取每根柱开盘时间,CopyHigh/CopyLow 取高低价;任意一次返回负值就打错误日志。注意这里写死 PERIOD_M1,所以你的直方图粒度被锁在 1 分钟级,想切 H1 得改源码。 开 MT5 随便挂个品种,把 range_percent 从默认调到 70 看重算耗时,能直观感受这套接口的性能边界。外汇和贵金属波动大,POC 仅是概率集中区,实盘须自担高风险。
class="type">void ShowSecondaryPOCs(class="type">bool show){show_second_poc=show;if(Init_passed)RefreshPOCs();} class="type">bool ShowSecondaryPOCs(){class="kw">return(show_second_poc);} class=class="str">"cmt">// 设置POC水平颜色 class="type">void ColorPOCs(class="type">class="kw">color col){poc_color=col; if(Init_passed)RefreshPOCs();} class="type">class="kw">color ColorPOCs(){class="kw">return(poc_color);} class=class="str">"cmt">// 设置直方图的内部颜色 class="type">void ColorInner(class="type">class="kw">color col); class="type">class="kw">color ColorInner(){class="kw">return(inner_color);} class=class="str">"cmt">// 设置直方图的外部颜色 class="type">void ColorOuter(class="type">class="kw">color col); class="type">class="kw">color ColorOuter(){class="kw">return(outer_color);} class=class="str">"cmt">// 设置百分比范围 class="type">void RangePercent(class="type">uint percent){range_percent=percent; if(Init_passed)calculationPOCs();} class="type">uint RangePercent(){class="kw">return(range_percent);} class=class="str">"cmt">// 返回POC水平原始值 class="type">bool VirginPOCs(){class="kw">return(virgin);} class=class="str">"cmt">// 返回直方图的构建时间 class="type">class="kw">datetime GetStartDateTime(){class="kw">return(time_start);} class=class="str">"cmt">// 更新POC水平 class="type">bool RefreshPOCs(); class="kw">private: class=class="str">"cmt">// 计算直方图和POC水平 class="type">bool calculationPOCs(); class=class="str">"cmt">// 删除类 class="type">void Delete(); }; class="type">bool CPriceHistogram::Init(class="type">class="kw">datetime time_open,class="type">class="kw">datetime time_close,class="type">bool showhistogram) class="type">bool CPriceHistogram::calculationPOCs() class=class="str">"cmt">// 从 time_start 到 time_end 中获取数据 class="type">int err=class="num">0; do { class=class="str">"cmt">//--- 复制每个柱形的开盘时间 rates_time=CopyTime(NULL,PERIOD_M1,time_start,time_end,iTime); if(rates_time<class="num">0) PrintErrorOnCopyFunction("CopyTime",_Symbol,PERIOD_M1,GetLastError()); class=class="str">"cmt">//--- 复制每个柱形的最高价 rates_high=CopyHigh(NULL,PERIOD_M1,time_start,time_end,iHigh); if(rates_high<class="num">0) PrintErrorOnCopyFunction("CopyHigh",_Symbol,PERIOD_M1,GetLastError()); class=class="str">"cmt">//--- 复制每个柱形的最低价