开发回放系统(第 42 部分):图表交易项目(I)(基础篇)
「给图表交易项目搭个回放骨架」
在 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 里拖两次指标验证报错提示是否生效。
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 面板响应;外汇和贵金属品种波动大,这类交互脚本仅作辅助,实盘风险仍高。
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">//+------------------------------------------------------------------+