从头开始开发智能交易系统(第 13 部分):时序与交易(II)·进阶篇
◍ 用 Canvas 把交易时间画进副窗口
在 MT5 里想实时把每笔成交时间列在图表侧边,靠 ObjectCreate 太慢,直接走 Canvas 类更稳。下面这段把字体、画布尺寸和绘制逻辑封装成内联函数,开 EA 时调一次就能常驻显示。 FontSet 先设字体名和字号,再用字符 "M" 量出当前字宽字高,存进 m_TextInfos。注意 TextGetSize 量的是单字基准,后续换行间距都按 TextHeight() 累加,避免不同字体下文字叠在一起。 PrintTimeTrade 里先用 Erase(clrBlack, 220) 清屏,透明度 220 让底色半透。循环上限 c0<=255,每行 y 步进一个 TextHeight(),最多列 256 条;成交标记 flag 为 0 显浅蓝、大于 0 显绿、小于 0 显砖红,对应未方向/多/空。 Init 中若副窗口不存在,设 "Lucida Console" 13 号字,区域宽按 18 个字符宽加 4 像素算(18 * TextWidth() + 4),再 Create 画布高度取终端屏幕高。外汇与贵金属杠杆高,这类悬浮信息仅辅助盯盘,不替代风控。
class="kw">inline class="type">void FontSet(class="kw">const class="type">class="kw">string name, class="kw">const class="type">int size, class="kw">const class="type">uint flags = class="num">0, class="kw">const class="type">uint angle = class="num">0) { if(!TextSetFont(name, size, flags, angle)) class="kw">return; TextGetSize("M", m_TextInfos.width, m_TextInfos.height); } class="kw">inline class="type">void TextOutFast(class="type">int x, class="type">int y, class="type">class="kw">string text, class="kw">const class="type">uint clr, class="type">uint alignment = class="num">0) { TextOut(text, x, y, alignment, m_Pixel, m_width, m_height, clr, COLOR_FORMAT_ARGB_NORMALIZE); } class="kw">inline class="type">int TextWidth(class="type">void) class="kw">const { class="kw">return m_TextInfos.width; } class="kw">inline class="type">int TextHeight(class="type">void) class="kw">const { class="kw">return m_TextInfos.height; } class="type">void PrintTimeTrade(class="type">void) { class="type">int ui1; m_InfoCanvas.Canvas.Erase(clrBlack, class="num">220); for (class="type">int c0 = class="num">0, c1 = m_CountStrings - class="num">1, y = class="num">2; (c0 <= class="num">255) && (y < m_InfoCanvas.MaxY); c0++, c1--, y += m_InfoCanvas.Canvas.TextHeight()) if (m_InfoTrades[macro_Limits(c1)].szTime == NULL) class="kw">break; else { ui1 = m_InfoTrades[macro_Limits(c1)].flag; m_InfoCanvas.Canvas.TextOutFast(class="num">2, y, m_InfoTrades[macro_Limits(c1)].szTime, macroColorRGBA((ui1 == class="num">0 ? clrLightSkyBlue : (ui1 > class="num">0 ? clrForestGreen : clrFireBrick)), class="num">220)); } m_InfoCanvas.Canvas.Update(); } class="type">void Init(class="kw">const class="type">int iScale = class="num">2) { if (!ExistSubWin()) { m_InfoCanvas.Canvas.FontSet("Lucida Console", class="num">13); m_InfoCanvas.WidthRegion = (class="num">18 * m_InfoCanvas.Canvas.TextWidth()) + class="num">4; CreateCustomSymbol(); CreateChart(); m_InfoCanvas.Canvas.Create(m_InfoCanvas.szNameCanvas, m_InfoCanvas.PosXRegion, class="num">0, m_InfoCanvas.WidthRegion, TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT), GetIdSubWinEA()); Resize(); m_ConnectionStatus = class="num">0; }
「让信息面板跟着图表尺寸走」
在 MT5 里做自定义信息面板,最容易被忽略的是窗口缩放时的重绘逻辑。下面这段把图表像素宽高读出来再反推对象尺寸,能在你拖拽子窗口时避免画布错位。 Resize 函数先用 ChartGetInteger 取当前图表宽高像素,其中 CHART_HEIGHT_IN_PIXELS 决定面板纵向拉伸上限,CHART_WIDTH_IN_PIXELS 减去预留区域 WidthRegion 后才是画布可用宽度。若宽度较上次有变化(MaxX != x),才重写 OBJPROP_XSIZE 与 OBJPROP_XDISTANCE,避免每帧无谓调用。 Update 里用 CopyTicks 拉全量 tick,循环跳过与上一次 m_MemTickTime 相同的旧 tick,只对新增部分计数。nSwap 作为静态计数器累计处理批次,lg1 记录相邻 tick 的毫秒差,用于后续节奏判断。外汇与贵金属报价受流动性影响,tick 间隔可能从 10ms 跳到 2s,这类面板仅作辅助观察,实盘仍属高风险。 别在每帧都重写坐标 静态变量 MaxX 的存在就是为拦掉重复设置。每帧调用 ObjectSetInteger 虽不会立刻卡死,但在 1 分钟 60+ 帧的 EURUSD 行情里,多余 COM 调用会拖慢 EA 主循环,倾向只在宽度真变时才动对象属性。
ObjectSetInteger(Terminal.Get_ID(), m_szObjName, OBJPROP_CHART_SCALE, (iScale > class="num">5 ? class="num">5 : (iScale < class="num">0 ? class="num">0 : iScale))); } class="type">void Resize(class="type">void) { class="kw">static class="type">int MaxX = class="num">0; class="type">int x = (class="type">int) ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS, GetIdSubWinEA()); m_InfoCanvas.MaxY = (class="type">int) ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS, GetIdSubWinEA()); ObjectSetInteger(Terminal.Get_ID(), m_szObjName, OBJPROP_YSIZE, m_InfoCanvas.MaxY); if (MaxX != x) { MaxX = x; x -= m_InfoCanvas.WidthRegion; ObjectSetInteger(Terminal.Get_ID(), m_szObjName, OBJPROP_XSIZE, x); ObjectSetInteger(Terminal.Get_ID(), m_InfoCanvas.szNameCanvas, OBJPROP_XDISTANCE, x); } PrintTimeTrade(); } class="kw">inline class="type">void Update(class="type">void) { class="type">MqlTick Tick[]; class="type">MqlRates Rates[def_SizeBuff]; class="type">int i0, p1, p2 = class="num">0; class="type">int iflag; class="type">long lg1; class="kw">static class="type">int nSwap = class="num">0; class="kw">static class="type">long lTime = class="num">0; if (m_ConnectionStatus < class="num">3) class="kw">return; if ((i0 = CopyTicks(Terminal.GetFullSymbol(), Tick, COPY_TICKS_ALL, m_MemTickTime, def_SizeBuff)) > class="num">0) { for (p1 = class="num">0, p2 = class="num">0; (p1 < i0) && (Tick[p1].time_msc == m_MemTickTime); p1++); for (class="type">int c0 = p1, c1 = class="num">0; c0 < i0; c0++) { lg1 = Tick[c0].time_msc - lTime; nSwap++; if (Tick[c0].volume == class="num">0) class="kw">continue; iflag = class="num">0; iflag += ((Tick[c0].flags & TICK_FLAG_BUY) == TICK_FLAG_BUY ? class="num">1 : class="num">0);
逐笔tick合成自定义分钟K的收尾拼接
上面这段逻辑发生在 tick 循环末尾,职责是把过滤后的单笔 tick 落进自合成 Rates 数组,并顺手记一笔成交痕迹。 iflag 先扣掉卖tick标记:若是卖单则减1,否则不减;当 iflag 归零说明该笔无方向意义,直接 continue 跳过,不写柱。 Rates[c1] 的高、低、开分别取 ask、bid、last;收价则按 last 加偏移:偏移量 = min(volume,200) × (WDO品种0.02否则1.0) × iflag。也就是说单笔超 200 手的成交量会被截断到 200,避免个别巨单把合成收价拉畸。 时间轴用 m_LastTime 打戳后自增 60,即每根合成柱固定跨 1 分钟(60秒)。循环结束调 CustomRatesUpdate 把 p2 根柱推到 m_szCustomSymbol 离线品种上,MT5 里切到该品种就能看到实时重绘的自定义序列。 绿色高亮部分是成交日志:用 StringFormat 把"分.毫秒 ~ 序号 <>成交量"写进 m_InfoTrades,nSwap 计同秒内第几笔,写完后归零、lTime 刷新为当前笔时间。外汇与贵金属杠杆高,自合成序列仅作盘后验证,实盘信号须自担风险。
iflag -= ((Tick[c0].flags & TICK_FLAG_SELL) == TICK_FLAG_SELL ? class="num">1 : class="num">0); if (iflag == class="num">0) class="kw">continue; Rates[c1].high = Tick[c0].ask; Rates[c1].low = Tick[c0].bid; Rates[c1].open = Tick[c0].last; Rates[c1].close = Tick[c0].last + ((Tick[c0].volume > class="num">200 ? class="num">200 : Tick[c0].volume) * (Terminal.GetTypeSymbol() == C_Terminal::WDO ? class="num">0.02 : class="num">1.0) * iflag); Rates[c1].time = m_LastTime; m_InfoTrades[macro_Limits(m_CountStrings)].szTime = StringFormat("%class="num">02.d.%03d ~ %class="num">02.d <>%class="num">04.d", ((lg1 - (lg1 % class="num">1000)) / class="num">1000) % class="num">60 , lg1 % class="num">1000, nSwap, Tick[c0].volume); m_InfoTrades[macro_Limits(m_CountStrings)].flag = iflag; m_CountStrings++; nSwap = class="num">0; lTime = Tick[c0].time_msc; p2++; c1++; m_LastTime += class="num">60; } CustomRatesUpdate(m_szCustomSymbol, Rates, p2); m_MemTickTime = Tick[i0 - class="num">1].time_msc; } PrintTimeTrade(); } lg1 = Tick[c0].time_msc - lTime; nSwap++; m_InfoTrades[macro_Limits(m_CountStrings)].szTime = StringFormat("%class="num">02.d.%03d ~ %class="num">02.d <>%class="num">04.d", ((lg1 - (lg1 % class="num">1000)) / class="num">1000) % class="num">60 , lg1 % class="num">1000, nSwap, Tick[c0].volume); m_InfoTrades[macro_Limits(m_CountStrings)].flag = iflag; m_CountStrings++; nSwap = class="num">0; lTime = Tick[c0].time_msc;
◍ 把工具请下神坛
这套「时序与交易」思路本质上只是把 MT5 原生图表的数据建模方式换了一层:不去盯 1 分钟 K 线讲了什么故事,而是把报价精度拉到毫秒级,看成交如何在最小时间颗粒里堆出微结构。很多人以为做分钟级交易就摸到了市场脉搏,其实一分钟内发生的挂单、撮合、撤单,才是真正驱动方向的那层底噪。 在 MT5 里打开任意品种,切到 M1 周期,再翻看成交明细的时间戳,你会看到大量亚秒级跳动根本没被常规蜡烛收纳。系统附带的 EA 压缩包约 5.98 MB,装进去跑一遍,就能直观比对「看分钟」和「看毫秒」两种视角下漏掉的信号数量。 外汇与贵金属本身杠杆高、滑点随机,毫秒数据也只是把盲区缩小一圈,不保证胜率。把它当放大镜,而不是水晶球。