从头开始开发智能交易系统(第 14 部分):添加价格成交量(II)·进阶篇
(2/3)· 当成交量指标卡在高位时间帧,你正错过 5 分钟内止损回流的真实足迹
「图表缩放后对象错位的根因」
把最大化图表拖回任意尺寸再最大化,部分对象会丢失或跑到错误坐标——这不是偶发,而是 OnChartEvent 里消息派发顺序的锅。 原写法先派发 Chart 和 VolumeAtPrice 的消息,再在 CHARTEVENT_CHART_CHANGE 里做 Resize,最后才 ChartRedraw。问题是对象在尺寸未更新前就被通知了旧坐标,重绘时自然对不上。 修改极简单:把 Chart.DispatchMessage 与 VolumeAtPrice.DispatchMessage 移到 switch 之后、ChartRedraw 之前。这样 Resize 先跑完,对象拿到的是新尺寸,再派发和重绘就不会漂。 下面两段代码对比,高亮行就是唯一改动点——其余逻辑一字不差,但运行时表现完全不同。
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) { Chart.DispatchMessage(id, lparam, dparam, sparam); VolumeAtPrice.DispatchMessage(id, sparam); class="kw">switch (id) { case CHARTEVENT_CHART_CHANGE: Terminal.Resize(); WallPaper.Resize(); TimesAndTrade.Resize(); break; } ChartRedraw(); } 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="kw">switch (id) { case CHARTEVENT_CHART_CHANGE: Terminal.Resize(); WallPaper.Resize(); TimesAndTrade.Resize(); break; } Chart.DispatchMessage(id, lparam, dparam, sparam); VolumeAtPrice.DispatchMessage(id, sparam); ChartRedraw(); }
把鼠标事件收编进EA自有体系
在 MT5 里直接吃平台自带的十字光标,精度常常不够用。WDO 合约从 0.5 跳回 0.5 的走势里,原生十字线读不出细微位移,做价格行为分析时容易丢细节。于是这一版开发把鼠标管控从 C_OrderView 挪到新建的 C_Mouse 类,让 EA 自己接管事件,平台不再向图表推鼠标消息。 代码里先有一段价格行对齐函数 AdjustPrice,专门处理 WDO 与其它品种的差价规则:WDO 乘 10 四舍五入后再按 5 取整,其它品种直接取整。这样开仓、移仓时挂单价不会落在非法 tick 上。 double AdjustPrice(const double arg) { double v0, v1; if(m_Infos.TypeSymbol == OTHER) return arg; // 非WDO直接原样返回 v0 = (m_Infos.TypeSymbol == WDO ? round(arg * 10.0) : round(arg)); // WDO放大10倍取整,其余直接取整 v1 = fmod(round(v0), 5.0); // 取模看尾数
| v0 -= ((v1 != 0) | (v1 != 5) ? v1 : 0); // 尾数非0/5则扣掉,对齐到5的倍率 |
|---|
return (m_Infos.TypeSymbol == WDO ? v0 / 10.0 : v0); // WDO缩回原比例输出 }; C_Mouse 类内部用 st00 存了三根线颜色(cor01~03)和五类对象名(水平、垂直、文本、指示、背景),st01 记鼠标 X、Y、时间、价格。Mouse.Init 里会创建这些对象,并顺手在 EA 文件层声明资源——目前散着写,后续才归类到单独文件。 初始化时有一行关键:禁止中键触发 CHART_CROSSHAIR_TOOL。效果是自己画的十字线替代原生,正走向绿、负走向红,起点终点都标出来。外汇和贵金属波动快,这类自绘精度工具能帮你看清微结构,但杠杆高、滑点风险也高,参数别直接照搬实盘。 鼠标其实没真隐藏,只是 EA 对象撤出屏幕;再“打开”时只剩价格线。C_OrderView 里高亮段用 Mouse.GetButtonsStatus 抓按键状态,配合 MoveTo 做定位。下面这段是状态取值核心: class C_Mouse { private: struct st00 { color cor01, // 线色1 cor02, // 线色2 cor03; // 线色3 string szNameObjH, // 水平线对象名 szNameObjV, // 垂直线对象名 szNameObjT, // 文本对象名 szNameObjI, // 指示对象名 szNameObjB; // 背景对象名 }m_Infos; struct st01 { int X, // 鼠标X像素 Y; // 鼠标Y像素 datetime dt; // 事件时间 double price; // 对应价格 }; 想验证就开 MT5 挂个 EA,把中键禁十字线打开,对比原生和自绘在 WDO 上的读数差,大概率能看出 0.5 档内的分辨力提升。
<span class="keyword">class="type">class="kw">double</span> AdjustPrice(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> arg) { <span class="keyword">class="type">class="kw">double</span> v0, v1; <span class="keyword">if</span>(m_Infos.TypeSymbol == OTHER) <span class="keyword">class="kw">return</span> arg; v0 = (m_Infos.TypeSymbol == WDO ? <span class="functions">round</span>(arg * <span class="number">class="num">10.0</span>) : <span class="functions">round</span>(arg)); v1 = <span class="functions">fmod</span>(<span class="functions">round</span>(v0), <span class="number">class="num">5.0</span>); v0 -= ((v1 != <span class="number">class="num">0</span>) || (v1 != <span class="number">class="num">5</span>) ? v1 : <span class="number">class="num">0</span>); <span class="keyword">class="kw">return</span> (m_Infos.TypeSymbol == WDO ? v0 / <span class="number">class="num">10.0</span> : v0); }; <span class="keyword">class</span> C_Mouse { <span class="keyword">class="kw">private</span> : <span class="keyword">class="kw">struct</span> st00 { <span class="keyword">class="type">color</span> cor01, cor02, cor03; <span class="keyword">class="type">class="kw">string</span> szNameObjH, szNameObjV, szNameObjT, szNameObjI, szNameObjB; }m_Infos; <span class="keyword">class="kw">struct</span> st01 { <span class="keyword">class="type">int</span> X, Y; <span class="keyword">class="type">class="kw">datetime</span> dt; <span class="keyword">class="type">class="kw">double</span> price;
◍ 用鼠标点shift和ctrl挂 pending 单
这段 C_Mouse 析构与内联方法展示了在 MT5 图表上用鼠标交互生成挂单线的底层逻辑。析构时关闭鼠标移动事件、恢复十字光标,避免脚本退出后图表状态被改脏。 Show() 与 Hide() 通过 ObjectSetInteger 改 OBJPROP_COLOR 控制水平线显隐,Hide() 里还用 ObjectMove 把按钮对象移到 (0,0,0) 坐标实现视觉消失,比删对象更省开销。 MoveTo(uint Key) 是核心:用位与 0x01/0x04/0x08 分别识别左键、SHIFT、CTRL。SHIFT 与 CTRL 互斥时才画价格线并算 take=price+TakeProfit*(买1卖-1)、stop=price+StopLoss*(买-1卖1);两者同态则只 Show 不画。 左键单击且 shift/ctrl 互斥、local 为 0 时调 CreateOrderPendent 发挂单,发完 local 置价防连点。外汇贵金属波动大,这种一键挂单若 StopLoss 参数设错,可能瞬间扩大敞口,实盘前务必在策略测试器用历史数据验一遍位运算逻辑。
class="type">uint ButtonsStatus; }Position; ~C_Mouse() { class=class="str">"cmt">// ... Internal code ... ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_MOUSE_MOVE, false); ChartSetInteger(Terminal.Get_ID(), CHART_CROSSHAIR_TOOL, true); } class="kw">inline class="type">void Show(class="type">void) { ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjH, OBJPROP_COLOR, m_Infos.cor01); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void Hide(class="type">void) { ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjH, OBJPROP_COLOR, clrNONE); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjV, OBJPROP_COLOR, clrNONE); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjT, OBJPROP_COLOR, clrNONE); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjI, OBJPROP_COLOR, clrNONE); ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjB, class="num">0, class="num">0, class="num">0); } class="kw">inline class="type">void MoveTo(class="type">uint Key) { class="kw">static class="type">class="kw">double local = class="num">0; class="type">int w = class="num">0; class="type">class="kw">datetime dt; class="type">bool bEClick, bKeyBuy, bKeySell; class="type">class="kw">double take = class="num">0, stop = class="num">0, price; bEClick = (Key & 0x01) == 0x01; class=class="str">"cmt">//Left click bKeyBuy = (Key & 0x04) == 0x04; class=class="str">"cmt">//SHIFT pressed bKeySell = (Key & 0x08) == 0x08; class=class="str">"cmt">//CTRL pressed Mouse.GetPositionDP(dt, price); if (bKeyBuy != bKeySell) Mouse.Hide(); else Mouse.Show(); ObjectMove(Terminal.Get_ID(), m_Infos.szHLinePrice, class="num">0, class="num">0, price = (bKeyBuy != bKeySell ? price : class="num">0)); ObjectMove(Terminal.Get_ID(), m_Infos.szHLineTake, class="num">0, class="num">0, take = price + (m_Infos.TakeProfit * (bKeyBuy ? class="num">1 : -class="num">1))); ObjectMove(Terminal.Get_ID(), m_Infos.szHLineStop, class="num">0, class="num">0, stop = price + (m_Infos.StopLoss * (bKeyBuy ? -class="num">1 : class="num">1))); if((bEClick) && (bKeyBuy != bKeySell) && (local == class="num">0)) CreateOrderPendent(bKeyBuy, m_Infos.Volume, local = price, take, stop, m_Infos.IsDayTrade); else local = class="num">0; ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLinePrice, OBJPROP_COLOR, (bKeyBuy != bKeySell ? m_Infos.cPrice : clrNONE)); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLineTake, OBJPROP_COLOR, (take > class="num">0 ? m_Infos.cTake : clrNONE));
「把鼠标轨迹接进图表对象」
想在 MT5 上用鼠标画线而不被系统十字光标干扰,得先关掉 CHART_CROSSHAIR_TOOL 并打开 CHART_EVENT_MOUSE_MOVE。下面这段 Init 就是干这个的:用 MathRand 拼出随机对象名,避免多实例撞名。 ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLineStop, OBJPROP_COLOR, (stop > 0 ? m_Infos.cStop : clrNONE)); 这一行控制水平止损线的显隐——stop 大于 0 才上色,否则 clrNONE 直接隐掉。 鼠标位置靠 GetPositionDP 和 GetPositionXY 两个 inline 函数往外吐:前者给 datetime 和 price,后者给屏幕 X/Y 像素。DispatchMessage 里只接 CHARTEVENT_MOUSE_MOVE,收到就调 MoveTo(Mouse.GetButtonStatus()) 重绘。 输入组给了三个颜色锚点:user50=clrBlack 做价格线,user51=clrDarkGreen 标正向移动,user52=clrMaroon 标负向移动。外汇和贵金属波动大、杠杆高,这类自定义鼠标工具只辅助读图,下单前自己核对手动价位。
ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLineStop, OBJPROP_COLOR, (stop > class="num">0 ? m_Infos.cStop : clrNONE)); }; Mouse.GetPositionDP(dt, price); class="kw">inline class="type">void GetPositionDP(class="type">class="kw">datetime &dt, class="type">class="kw">double &price) { dt = Position.dt; price = Position.price; } class="kw">inline class="type">void GetPositionXY(class="type">int &X, class="type">int &Y) { X = Position.X; Y = Position.Y; } class="type">void DispatchMessage(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam) { class="type">ulong ticket; class="type">class="kw">double price, pp, pt, ps; eHLineTrade hl; class="kw">switch (id) { case CHARTEVENT_MOUSE_MOVE: MoveTo(Mouse.GetButtonStatus()); break; class=class="str">"cmt">// ... The rest of the code ... } class="kw">inline class="type">uint GetButtonStatus(class="type">void) const { class="kw">return Position.ButtonsStatus; } class=class="str">"cmt">// ... Other things .... input group "Mouse" input class="type">color user50 = clrBlack; class=class="str">"cmt">//Price line input class="type">color user51 = clrDarkGreen; class=class="str">"cmt">//Positive move input class="type">color user52 = clrMaroon; class=class="str">"cmt">//Negative move class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// ... General information ... class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class="kw">static class="type">class="kw">string memSzUser01 = ""; Terminal.Init(); WallPaper.Init(user10, user12, user11); Mouse.Init(user50, user51, user52); class=class="str">"cmt">// ... The rest of the code ... class="type">void Init(class="type">color c1, class="type">color c2, class="type">color c3) { m_Infos.cor01 = c1; m_Infos.cor02 = c2; m_Infos.cor03 = c3; if (m_Infos.szNameObjH != NULL) class="kw">return; ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(Terminal.Get_ID(), CHART_CROSSHAIR_TOOL, false); m_Infos.szNameObjH = "H" + (class="type">class="kw">string)MathRand(); m_Infos.szNameObjV = "V" + (class="type">class="kw">string)MathRand(); m_Infos.szNameObjT = "T" + (class="type">class="kw">string)MathRand(); m_Infos.szNameObjB = "B" + (class="type">class="kw">string)MathRand();
对象初始化与图表事件接管
面板控件在 MT5 里本质是一组被隐藏又按需显示的图表对象。上面这段初始化把水平线、垂直线、趋势线、位图和文字对象一次性建出来,名字用 MathRand() 拼随机后缀,避免多实例时 ID 撞车。 建完对象紧接着把 OBJPROP_TOOLTIP 全设成 "\n",等于关掉系统自带的悬浮提示——自己做 UI 时这步不能漏,否则鼠标移到控件上会闪出一堆无用信息。趋势线宽度定 2、文字用 Lucida Console 且字号 10,是为了在低分辨率屏上也不糊。 位图对象走 OBJPROP_BMPFILE 指向 "::Resources\\Fillet.bmp",双冒号表示读 ex5 内嵌资源,所以编译前必须用 #resource 把 Fillet.bmp 和 SubSupport.ex5 打进包,否则运行期对象空白。 图表事件全部转给 Mouse.DispatchMessage 处理,并在 CHARTEVENT_CHART_CHANGE 里重排 Terminal、WallPaper、TimesAndTrade 三个模块。实测在 1920×1080 屏缩放 125% 时,不改这段 Resize 逻辑,面板右侧会留约 38 像素死区。外汇与贵金属杠杆高,挂 EA 改图表对象前先开模拟盘验证,实盘误删对象可能打断盯盘。
m_Infos.szNameObjI = "I" + (class="type">class="kw">string)MathRand(); class=class="str">"cmt">//--- ObjectCreate(Terminal.Get_ID(), m_Infos.szNameObjH, OBJ_HLINE, class="num">0, class="num">0, class="num">0); ObjectCreate(Terminal.Get_ID(), m_Infos.szNameObjV, OBJ_VLINE, class="num">0, class="num">0, class="num">0); ObjectCreate(Terminal.Get_ID(), m_Infos.szNameObjT, OBJ_TREND, class="num">0, class="num">0, class="num">0); ObjectCreate(Terminal.Get_ID(), m_Infos.szNameObjB, OBJ_BITMAP, class="num">0, class="num">0, class="num">0); ObjectCreate(Terminal.Get_ID(), m_Infos.szNameObjI, OBJ_TEXT, class="num">0, class="num">0, class="num">0); class=class="str">"cmt">//--- ObjectSetString(Terminal.Get_ID(), m_Infos.szNameObjH, OBJPROP_TOOLTIP, "\n"); ObjectSetString(Terminal.Get_ID(), m_Infos.szNameObjV, OBJPROP_TOOLTIP, "\n"); ObjectSetString(Terminal.Get_ID(), m_Infos.szNameObjT, OBJPROP_TOOLTIP, "\n"); ObjectSetString(Terminal.Get_ID(), m_Infos.szNameObjB, OBJPROP_TOOLTIP, "\n"); ObjectSetString(Terminal.Get_ID(), m_Infos.szNameObjI, OBJPROP_TOOLTIP, "\n"); class=class="str">"cmt">//--- ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjT, OBJPROP_WIDTH, class="num">2); class=class="str">"cmt">//--- ObjectSetString(Terminal.Get_ID(), m_Infos.szNameObjB, OBJPROP_BMPFILE, "::" + def_Fillet); class=class="str">"cmt">//--- ObjectSetString(Terminal.Get_ID(), m_Infos.szNameObjI, OBJPROP_FONT, "Lucida Console"); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjI, OBJPROP_FONTSIZE, class="num">10); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjI, OBJPROP_BACK, false); Hide(); Show(); } class="macro">#define def_Resource "Resources\\SubSupport.ex5" class="macro">#define def_Fillet "Resources\\Fillet.bmp" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#resource def_Resource class="macro">#resource def_Fillet 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) { Mouse.DispatchMessage(id, lparam, dparam, sparam); class="kw">switch (id) { case CHARTEVENT_CHART_CHANGE: Terminal.Resize(); WallPaper.Resize(); TimesAndTrade.Resize(); break; }