从头开始开发智能交易系统(第 13 部分):时序与交易(II)·进阶篇
📘

从头开始开发智能交易系统(第 13 部分):时序与交易(II)·进阶篇

第 2/2 篇

◍ 用 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 画布高度取终端屏幕高。外汇与贵金属杠杆高,这类悬浮信息仅辅助盯盘,不替代风控。

MQL5 / C++
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 主循环,倾向只在宽度真变时才动对象属性。

MQL5 / C++
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 刷新为当前笔时间。外汇与贵金属杠杆高,自合成序列仅作盘后验证,实盘信号须自担风险。

MQL5 / C++
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,装进去跑一遍,就能直观比对「看分钟」和「看毫秒」两种视角下漏掉的信号数量。 外汇与贵金属本身杠杆高、滑点随机,毫秒数据也只是把盲区缩小一圈,不保证胜率。把它当放大镜,而不是水晶球。

常见问题

用 Canvas 在独立副窗口按时间区间填色,避免遮挡主图价格;先算会话起止时间再循环画矩形即可。
监听图表尺寸变化事件,用相对坐标(百分比或右下角偏移)重绘面板,别写死像素位置。
小布盯盘的 AIGC 可识别副窗口时段标注与面板布局异常,打开对应品种页就能直接看诊断结果。
在会话结束或定时器触发时强制 flush 未闭合桶,把缓存 tick 按时间归桶后补收尾根。
贵金属跳空和流动性断层多,时段切分可能漏 tick;结论仅作概率参考,实盘前先模拟验证。