基于成交历史的交易播放器·进阶篇
(2/3)· 想在 MT5 里把历史成交像视频一样回放?本篇拆解对象通信与多品种同步的进阶实现
◍ 先把播放器的需求框定清楚
做交易历史播放器,第一步不是写代码,而是把用户侧要的属性列成规范。起点永远是「这个应用该长什么样」,而不是技术实现。 核心属性至少有这几条:多货币支持、自动打开所需图表、可双向滚动的导航界面、所有图表同步显示;在此之上还要能开始/暂停播放,自选显示几个图表和品种,自选周期,并在图上画出成交历史与余额/净值曲线,甚至能拆出单品种和总账户的净值。 前四条决定产品形态,后面那些决定代码往哪走。整体开发路线是先加载 HTML 报告,按成交解析并复原仓位历史,再把建仓平仓整理成序列,按用户指令用指标形式动态回放,并挂信息面板。 若数据源是 MT4 报告、要在 MT5 策略测试里跑,还得配一个 EA:把解析后的成交写成二进制文件,再生成 MT5 测试报告。这套规范定下来,后续就能自顶向下排代码,不必盲写。
MT5 的仓位容器与历史重组逻辑
MT4 把一笔交易塞进一个“订单容器”:门开着就是在市,门关上整条记录进历史。MT5 换了思路,容器叫仓位(Position),但不再单独留仓位历史,只剩订单和成交的公共流水。 这意味着你从 MT4 迁过来,第一道坎不是写代码,而是重新组织历史数据的脑子——所有仓位轨迹都要靠订单与成交反推。 具体挂钩靠两个标识符:ORDER_POSITION_ID 标订单归属,DEAL_POSITION_ID 标成交归属。要把 MT4 历史洗成 MT5 能用的格式,实操上就是把原来一条订单拆成两条独立交易——建仓和平仓各算一笔。 外汇与贵金属杠杆高、滑点随机,这种拆法只解决记录结构,不暗示任何胜率改善,回测前务必在 MT5 历史中心核对品种点值。
「HTML 解析器」
对于不熟悉计算机行话的那些人而言,我将说明“解析”一词是什么意思。解析指对文本或任意顺序的词位(符号、单词、字节等)进行句法(文法或词汇)分析,检查输入文本与指定文法的对应,并构建一个解析树,您可以据此执行进一步的计算或转换。 在解析器中使用了两个大类 CTable 和 CHTML。CTable 类的使用在“ MQL5 中的电子表格 ”一文中详细说明,因此我不再说明该类。 为了解析 HTML,我开发了 CHTML 类。依据我的初步想法,应撰写一篇文章来对其进行说明。但是对于撰写文章而言,这个类太简单了,因此我将对其进行简单的说明。 可以用术语“标记”来说明类的一般概念。一个标记可表示为具有包围的函数。例如 Tag(header,casket),其中 'header' 是标记的标题(通常在此处指定控制页面外观的标记变量),'casket' 是标记容器的内容。此类标记包含整个 HTML 语言。 类的一般结构可表示为对象的三阶段调用。CHTML 类的实例在其主体中创建所有可能标记的对象。标记的函数通过一个模板创建,它们仅在名称和两个标记的设置上有所不同。 一个标记确定 header 的存在,另一个确定 casket 的存在。这些标记允许用共同的结构表示所有标记。每个标记实例在其主体中创建 CTegs 类的一个实例。此类包含针对所有标记的公共方法,并且它执行在文档主体中搜索必要标记的主要操作。 因此,三阶段调用看起来如下所示: 此表达式表示 'h' 对象通过 'td' 对象(<td header >casket</td> 标记的实例),再通过嵌套对象 'base'(CTegs 类的对象)调用 'casket' 变量的值。 类还包含搜索标记的方法,它们在以下公共方法中组合在一起: 该方法返回标记结束处的搜索点,并填写标记的 'header' 和 'casket' 变量。 没有使用在类中准备的其他函数,因此我将不会说明它们,还有很多其他有趣的东西要讨论。 在使用 HTML 文档的说明结束之时,我想指出,在本文中使用了两类解析器;它们仅在保存从文件获取信息的类型方面有所不同。第一种使用将整个文档保存到一个类型为 'string' 的单一变量中。播放器使用这种方法。第二种对报告进行逐行解析。它在用于准备 2008 年锦标赛历史的脚本中使用。 为什么我要使用两种方法?问题在于要让 CTegs 类的函数能够正确运行,整个标记应放在分析后的字符串。然而并不是始终都能如此。例如,在 table、html、body 等标记中(它们是多行的)。字符串类型的变量允许存储 32750 个符号(依据我的计算),不包括制表符。通过 '\r'(在每 32748 个字符之后),我成功存储多达 2 000 000 个符号;在达到此值之后,我停止了我的尝试。很有可能还能存储更多符号。 那么,为什么我要使用两种方法?重点在于,对于播放器的通用解析器,您需要找到正确的表格。测试程序报告和成交历史报告的所需表格位于不同的地方。为了保持多功能性(从而让解析器能够同时理解两种报告),我采用的方案是在表格中搜索含有 "deals" 的 'td' 标记。
- 年锦标赛的报告结构是已知的,无需搜索必需的表格。然而,报告文档非常巨大 (35 MB),将整个报告存放在一个变量中要消耗大量时间。这种情形要求采用第二种方法进行解析。
[CODE] <p style="text-align:justify;">h.td.base.casket</p> <p style="text-align:justify;
◍ 用 OOP 关系搭出多图表播放器骨架
多货币回放首要的是让 EA 接管图表,每个图表交给一个独立对象处理最合理。历史数据不能现用现取,重复拉同一段历史纯属浪费,所以每个播放器对象要持有一份独立历史样本,保证回放不中断。 OOP 里先分清三种关系:关联是对象间的普通连接;聚合是“部分”关系且这部分可同时归多个所有者;组合是更严的聚合,部分专属一个所有者、所有者销毁时部分也销毁。MQL5 可用单参数传指针的函数把对象绑起来,OnInit 里大量用关联,CPlayer 类里聚合与组合混用——CChartData 和 SBase 用聚合给所有对象建公共数据字段。 自顶向下写:先按模板建空类、连起来测请求处理,再填功能,出错点一眼能定位。OnInit 中 CParser_Tester 解析测试报告、算持仓并在图表画历史;因画图要图表 ID,对象延后删除。Balance_Process() 基于 M1 算各品种余额净值,若某个品种历史没下载全就中断 EA 并提醒,产出二进制文件供后续指标切周期。 图表打开用函数传品种和周期,但返回值不一定可靠。我用 ChartTotal() 在 EA 启动前存已有图表 ID,CurrentChart() 比对差异拿到 EA 新建图表 ID,这套依据“图表已开”事实的方案更稳。 计时器是第一个坑:EventSetTimer 最小 1 秒,太慢,我在计时器内套毫秒级循环,但这样收不到事件、没法放指标里并行算。后来用 CClick 组合类发图表激活信号,Click(n) 循环里检测激活按钮(全图大小、背景色、底层),按下就切其他对象无反应。检测到主图后 Progress(Time) 算时间推进,CopyPlayer(n) 把主图设置拷到从图,Play(Time) 制造时间报价跳动。 组合类分工:CArrayRuler 存柱间快移信息,CRuler 存 M1 生成价动,CSpeed 控速度,CProgress 管进度按钮独占,CPlay 启停并控余额指标,CClick 管激活,CBackGround 遮零柱,CBarDraw 画零柱,CHistoryDraw 伪造成交实时感。CChartData 用聚合同步从图设置。 想加周期就改 Player5_1.mqh 里的静态数组 TFarray[] 和对应枚举、CountTF,重编译 EA 即可。下面这段 MQL5 演示了关联的最简形态:Base 类只有一个整型成员 a;A_Association 的方法 Association 接收 Base 指针,仅在传参的方法内通过该指针访问绑定对象数据,不长期持有。 class Base { public: Base(void){}; ~Base(void){}; int a; }; //+------------------------------------------------------------------+ class A_Association { public: A_Association(void){}; ~A_Association(void){}; void Association(Base *a_){}; // At association, data of the bound object // will be available through the object pointer only in the method, // where the pointer is passed. }; //+------------------------------------------------------------------+
class Base { class="kw">public: Base(class="type">void){}; ~Base(class="type">void){}; class="type">int a; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class A_Association { class="kw">public: A_Association(class="type">void){}; ~A_Association(class="type">void){}; class="type">void Association(Base *a_){}; class=class="str">"cmt">// At association, data of the bound object class=class="str">"cmt">// will be available through the object pointer only in the method, class=class="str">"cmt">// where the pointer is passed. }; class=class="str">"cmt">//+------------------------------------------------------------------+
聚合与组合的指针生命周期差异
在 MT5 的 EA 或指标工程里,对象关系常分聚合(aggregation)与组合(composition)两种。聚合只持有一个外部对象的指针,自身不负责其内存;组合则在构造函数里 new、析构函数里 delete,生命周期完全自闭。 看这段类定义:A_Aggregation 仅把外部传进来的 Base 指针存到成员 a,不分配也不释放;A_Composition 在构造时 a=new Base,析构时 delete a。前者适合多个模块共享同一份行情缓存,后者适合图表对象这类随类生灭的资源。 实际写 OnStart 时,用 GetPointer(a) 把栈对象交给关联类,要注意原对象作用域结束后的悬空风险。外汇与贵金属波动剧烈,这类指针误用可能在回测不显、实盘却随机崩溃,属于高风险坑点。 下面这段 _XXX_ 类把图表 id、品种名、基础数据指针和图表数据指针绑在一起,Create 里先做 ObjectFind 判断对象是否存在再决定创建,避免重复挂对象导致的图表闪烁。
class A_Aggregation { Base *a; class="kw">public: A_Aggregation(class="type">void){}; ~A_Aggregation(class="type">void){}; class="type">void Aggregation(Base *a_){a=a_;}; class=class="str">"cmt">// At aggregation, data of the bound object class=class="str">"cmt">// will be available through the object pointer in any method of the class. }; class A_Composition { Base *a; class="kw">public: A_Composition(class="type">void){ a=new Base;}; ~A_Composition(class="type">void){class="kw">delete a;}; class=class="str">"cmt">// At composition, the object becomes the class member. }; GetPointer(pointer) class="type">void OnStart() { Base a; A_Association b; b.Association(GetPointer(a)); } class _XXX_ { class="kw">private: class="type">long chart_id; class="type">class="kw">string name; SBase *s; CChartData *d; class="kw">public: class="type">bool state; _XXX_(){state=class="num">0;}; ~_XXX_(){}; class="type">void Create(class="type">long Chart_id, SBase *base, CChartData *data); class="type">void Update(); }; class="type">void _XXX_::Create(class="type">long Chart_id, SBase *base, CChartData *data) { chart_id=Chart_id; s=base; class=class="str">"cmt">// binding data to the player structure d=data; class=class="str">"cmt">// binding data to the chart structure name=" "+ChartSymbol(chart_id); if(ObjectFind(chart_id,name)<class="num">0)class=class="str">"cmt">// if there is no object yet {class=class="str">"cmt">//--- try to create the object