开发回放系统(第 42 部分):图表交易项目(I)(基础篇)
📘

开发回放系统(第 42 部分):图表交易项目(I)(基础篇)

第 1/2 篇

「给图表交易项目搭个回放骨架」

在 MT5 里做图表交易项目的回放系统,核心是把历史报价按可控节奏重新推送到当前图表,让交易者能像看实盘一样复盘自己的下单逻辑。这类项目通常从第 42 部分这类长系列教程中剥离出最小可用模块,先跑通再迭代。

  • 年 11 月 11 日发布的示例帖在站内拿到 493 次浏览、1 条评论,说明纯 MQL5 图表级回放的需求并不小,但愿意动手改代码的人很少。

开 MT5 新建一个 EA 骨架,用 CopyRates 把指定品种的历史柱拉进数组,再用自定义定时器按毫秒级间隔重绘,就能先验证回放不卡顿。外汇与贵金属杠杆高,回放仅用于验证策略逻辑,实盘仍面临滑点与断连风险。

别让模块在图表上内讧

很多人觉得给 MT5 写个鼠标控制指标纯属折腾,长远看没收益。其实每次新增程序都要解决它和既有模块共存的条件,能不能搞定这种共存,基本就是业余和专业的分水岭。 初学者常栽在模块冲突上:把一千零一种功能塞进一个智能系统,一旦叠在同一图表就互相报错,这种系统毫无意义;更糟的是,稳定程序被新程序复用部分功能后,组合运行直接爆一连串故障,排错耗时且部分极复杂。 我们做鼠标指标,本质不是在造『完美武器』,而是在攒兼容零部件——需要时组合出专门应对当前问题的工具。回放/模拟器系统的初衷,就是让训练环境和实盘 MetaTrader 5 用法完全一致,没人想在练兵时用一套、真刀真枪换另一套。 鼠标指标之后,先补两个工具:一个简单,一个要模拟数据所以复杂。简单的就是图表交易(Chart Trade),早先在《从头开始开发智能交易系统(第 30 部分)》里的版本过于玩具化、不通用;现在要写一版能和鼠标指标和谐共存的图表交易,外汇/贵金属高杠杆下模块冲突会放大滑点与误单风险,兼容是前提。

◍ 用 RAD 思路搭一个轻量图表交易壳

图表交易我们之前聊过基础,但这次不沿用旧模板,而是重写一个更紧凑的版本。代码量明显下降,后续改逻辑或修 bug 都更省事,核心是用鼠标指标去移动、点击、跟图表上的元素交互,写法跟多数人习惯的 MT5 套路差别很大。 这里借用了 RAD(Rapid Application Development)的概念——把精力放在代码逻辑上,而不是界面外观。前期文章里已经演示过如何在 MT5 里近似实现 RAD,本节的指标其实就是那个思路的简化落地版,后面要接的内容会比它复杂,但这一层是最容易啃的。 指标本体极简:唯一关键是第 7 行声明不画任何数据缓冲,其余只是 new 两个支撑类指针、退出时销毁。MT5 会把加载时传入的第一个字符串当指标名,第二个字符串指向鼠标指标名,这部分在回放系统相关文章里有细说。 防止同图重复加载靠 C_Terminal 里的片段。下面这段代码拼到类里,所有用到它的指标都能受益:临时名加 _TMP 试探,若图表已存在原名实例就删临时名并报错退出。 别把这段当万能锁 它只是默认拦掉重复实例,后面我们会用绕过方式做多实例,现在先不管。真正上线前你要在 MT5 里拖两次指标验证报错提示是否生效。

MQL5 / C++
class="num">156. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">157.                class="type">bool IndicatorCheckPass(class="kw">const class="type">class="kw">string szShortName)
class="num">158.                  {
class="num">159.                      class="type">class="kw">string szTmp = szShortName + "_TMP";
class="num">160.  
class="num">161.                      if (_LastError != ERR_SUCCESS) class="kw">return false;
class="num">162.                      IndicatorSetString(INDICATOR_SHORTNAME, szTmp);
class="num">163.                      if (ChartWindowFind(m_Infos.ID, szShortName) != -class="num">1)
class="num">164.                      {
class="num">165.                          ChartIndicatorDelete(m_Infos.ID, class="num">0, szTmp);
class="num">166.                          Print("Only one instance is allowed...");
class="num">167.  
class="num">168.                          class="kw">return false;
class="num">169.                      }
class="num">170.                      IndicatorSetString(INDICATOR_SHORTNAME, szShortName);
class="num">171.                      ResetLastError();
class="num">172.

「把浮动面板挂上图表事件循环」

这段脚本是图表交易指示器的骨架:它不画任何指标线(indicator_plots 设为 0),只负责把鼠标交互的浮动面板接进 MT5 的事件系统。 OnInit 里用 new 创建了 C_ChartFloatingRAD 对象,并内嵌一个 C_Mouse 实例;若 _LastError 不等于 ERR_SUCCESS 就返回 INIT_FAILED,否则 INIT_SUCCEEDED。版本号写在 1.42,说明这是 DEMO 形态的基础版。 OnCalculate 直接 return rates_total,等于不介入价格计算;真正的活儿在 OnChartEvent——把图表事件派发给 chart 对象后调 ChartRedraw 强制重绘。OnDeinit 里 delete chart 并再重绘一次,避免残留图形对象。 想验证就开 MT5 把 Market Replay\Chart Trader 里的两个 mqh 放好,挂上这个指示器,鼠标移上去应能看到浮动 RAD 面板响应;外汇和贵金属品种波动大,这类交互脚本仅作辅助,实盘风险仍高。

MQL5 / C++
class="num">173.                class="kw">return true;
class="num">174.            }
class="num">175. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">03. class="macro">#class="kw">property description "Base version for Chart Trade(DEMO version)"
class="num">04. class="macro">#class="kw">property version   "class="num">1.42"
class="num">05. class="macro">#class="kw">property link "[MQL5官方文档]
class="num">06. class="macro">#class="kw">property indicator_chart_window
class="num">07. class="macro">#class="kw">property indicator_plots class="num">0
class="num">08. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">09. class="macro">#include <Market Replay\Chart Trader\C_ChartFloatingRAD.mqh>
class="num">10. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">11. C_ChartFloatingRAD *chart = NULL;
class="num">12. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">13. class="type">int OnInit()
class="num">14. {
class="num">15.    chart = new C_ChartFloatingRAD("Indicator Chart Trade", new C_Mouse("Indicator Mouse Study"));
class="num">16.    if (_LastError != ERR_SUCCESS)
class="num">17.    {
class="num">18.       Print("Error number:", _LastError);
class="num">19.       class="kw">return INIT_FAILED;
class="num">20.    }
class="num">21.    
class="num">22.    class="kw">return INIT_SUCCEEDED;
class="num">23. }
class="num">24. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">25. class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[])
class="num">26. {
class="num">27.    class="kw">return rates_total;
class="num">28. }
class="num">29. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">30. class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
class="num">31. {
class="num">32.    (*chart).DispatchMessage(id, lparam, dparam, sparam);
class="num">33.    
class="num">34.    ChartRedraw();
class="num">35. }
class="num">36. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">37. class="type">void OnDeinit(class="kw">const class="type">int reason)
class="num">38. {
class="num">39.    class="kw">delete chart;
class="num">40.    
class="num">41.    ChartRedraw();
class="num">42. }
class="num">43. class=class="str">"cmt">//+------------------------------------------------------------------+

常见问题

先用轻量壳把回放事件循环和图表对象分离,别一上来就写复杂订单逻辑,先跑通时间轴拖动再补交易模块。
检查各模块的图表事件处理函数是否抢同一个对象 ID,统一用前缀命名并交出一个事件入口,避免内讧。
可以,把面板挂上图表事件循环后,让小布对照你的浮动面板代码跑一遍异常场景,标出事件冲突和空指针位置。
只保留拖拽、下单、撤单三个核心事件,把画图和分析拆成外挂,壳子文件控制在几百行内就算轻量。
是你没把面板坐标绑进图表重绘事件,在每帧刷新里重算锚点坐标,别只初始化时定位一次。