设为首页 收藏本站 切换语言

概述 在上一篇文章“开发回放系统 — 市场模拟(第 02 部分):首次实验(II)”当中,我们创建了一个系统,其可在足够的处理时 ...

| 发表于 2023-10-1 19:02:40 | 显示全部楼层 |复制链接
概述
在上一篇文章“开发回放系统 — 市场模拟(第 02 部分):首次实验(II)”当中,我们创建了一个系统,其可在足够的处理时间内生成 1 分钟的柱线,用来模拟市场。 然而,我们明白我们无法控制正在发生的事情。 我们的能力仅限于选择一些要点,并调整其它要点。 对于正在运行的系统,我们少有选择。

在本文中,我们将尝试改善这种情况。 我们将利用一些额外的控制来令我们的分析更易于管理。 虽然我们还有很多工作要做,才能获得一个在统计分析和图表控制方面功能齐全的系统,但这是一个良好的开端。

在本文中,我们只会做一些调整,因此相对会较短。 我们不会在此步骤中详细介绍。 我们的目标是为必要的控制奠定基础,以使回放更容易实现,并为那些想要将系统付诸实践的人进行分析。


规划
这个规划步骤非常简单,因为如果您查看上一篇文章中系统的工作原理,就会很清楚我们需要做什么。 我们需要创建一个控制窗体,在其中我们可以暂停、播放,最重要的是,选择一个特定的时间来开始研究。

在当前视图中,我们将始终从第一次交易跳价开始。 假设我们想自市场的第五个小时开始研究,即从 14:00(假设市场在 9:00 开盘)。 在这种情况下,我们将不得不等待回放 5 个小时,然后才执行必要的分析。 这是完全不可能的,因为如果我们试图停止回放,它将关闭,我们将不得不从第一次交易跳价重新开始。

现在很清楚我们需要马上做什么,因为它现在的工作方式令人沮丧,即使这个想法本身很有趣。

现在我们有了大致的方向,我们就可以继续实现。


实现
实现会非常有趣,因为我们将不得不经历从最简单到最多样化的不同路径,从而创建真正的控制系统。 不过,如果您仔细阅读解释,所有步骤都很容易理解,并按发布顺序遵循文章所说,无需跳过任何步骤,也无需尝试向前跨出若干步。


与许多人的想法相左,我们不会在系统中使用 DLL。 我们只使用纯 MQL5 语言实现回放系统。 这个思路是充分利用 MetaTrader 5,并展示在创建必要功能时我们可以在平台内走多远。 求助于外部实现会剥夺使用 MQL5 的很多乐趣,给人的印象是 MQL5 无法满足我们的需求。

如果您查看上一篇文章中所用的代码,您可以看到系统使用服务来创建回放。 它还包括启动它的脚本。 此脚本允许服务发送自定义交易品种的跳价,从而创建回放。 我们用到了一个简单的切换机制。 然而,这种方法不适合更有效的控制。 我们必须走一条更困难的道路。


创建超基本 EA
我们来尝试利用 EA 实现控制。 该 EA 将控制服务何时应该或不应该为柱线生成跳价。

为什么是 EA? 我们可以使用指标替代 EA,其工作方式相同。 不过,我想使用 EA,因为我们稍后需要它来创建订单模拟系统。 此外,我们将尝试使用我在另一系列文章称为“从头开始开发交易 EA”中介绍的订单系统。 我们现在不必担心订单系统,因为在我们开始之前我们还有很多工作要做。

我们基本 EA 的完整代码如下所示:

#property copyright "Daniel Jose"
#property version   "1.00"
//+------------------------------------------------------------------+
#include <Market Replay\C_Controls.mqh>
//+------------------------------------------------------------------+
C_Controls      Control;
//+------------------------------------------------------------------+
int OnInit()
{
        Control.Init();
               
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {}
//+------------------------------------------------------------------+
void OnTick() {}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Control.DispatchMessage(id, lparam, dparam, sparam);
}
//+------------------------------------------------------------------+


代码非常简单,但足以控制服务的操作。 现在我们来看一下代码的某些部分,即上面高亮显示的控制对象类。 在开发的早期阶段,代码并不是很复杂。 我们只实现一个按钮来播放和暂停回放服务。 如此,我们来看看这个当前开发阶段的类。

首先要注意的,如下所示:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include <Market Replay\Interprocess.mqh>
//+------------------------------------------------------------------+
#define def_ButtonPlay  "Images\\Market Replay\\Play.bmp"
#define def_ButtonPause "Images\\Market Replay\\Pause.bmp"
#resource "\\" + def_ButtonPlay
#resource "\\" + def_ButtonPause
//+------------------------------------------------------------------+
#define def_PrefixObjectName "Market Replay _ "


第一个要点是 《蓝色高亮 的头文件。 我们稍后会详细查看它。 然后,我们有一些位图对象的定义,这些对象将表示播放和暂停按钮。 这里没有什么太复杂的。 一旦定义了这些要点,我们就能迈进类代码,它们非常紧凑。 完整代码如下所示。

class C_Controls
{
        private :
//+------------------------------------------------------------------+
                string  m_szBtnPlay;
                long            m_id;
//+------------------------------------------------------------------+
                void CreateBtnPlayPause(long id)
                        {
                                m_szBtnPlay = def_PrefixObjectName + "Play";
                                ObjectCreate(id, m_szBtnPlay, OBJ_BITMAP_LABEL, 0, 0, 0);
                                ObjectSetInteger(id, m_szBtnPlay, OBJPROP_XDISTANCE, 5);
                                ObjectSetInteger(id, m_szBtnPlay, OBJPROP_YDISTANCE, 25);
                                ObjectSetInteger(id, m_szBtnPlay, OBJPROP_STATE, false);
                                ObjectSetString(id, m_szBtnPlay, OBJPROP_BMPFILE, 0, "::" + def_ButtonPause);
                                ObjectSetString(id, m_szBtnPlay, OBJPROP_BMPFILE, 1, "::" + def_ButtonPlay);
                        }
//+------------------------------------------------------------------+
        public  :
//+------------------------------------------------------------------+
                C_Controls()
                        {
                                m_szBtnPlay = NULL;
                        }
//+------------------------------------------------------------------+
                ~C_Controls()
                        {
                                ObjectDelete(ChartID(), m_szBtnPlay);
                        }               
//+------------------------------------------------------------------+
                void Init(void)
                        {
                                if (m_szBtnPlay != NULL) return;
                                CreateBtnPlayPause(m_id = ChartID());
                                GlobalVariableTemp(def_GlobalVariableReplay);
                                ChartRedraw();
                        }
//+------------------------------------------------------------------+
                void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
                        {
                                u_Interprocess Info;
                                
                                switch (id)
                                {
                                        case CHARTEVENT_OBJECT_CLICK:
                                                if (sparam == m_szBtnPlay)
                                                {
                                                        Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                                        GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
                                                }
                                                break;
                                }
                        }
//+------------------------------------------------------------------+
};


这里我们有两个主要函数:Init 和 DispatchMessage。它们在这个早期阶段实现 EA 操作所需的所有工作。 为了更好地解释其中的一些细节,我们来看看下面的这两个函数。 我们从 Init 开始。

void Init(void)
{
        if (m_szBtnPlay != NULL) return;
        CreateBtnPlayPause(m_id = ChartID());
        GlobalVariableTemp(def_GlobalVariableReplay);
        ChartRedraw();
}

调用 Init 时,它首先检查之前是否已创建控制元素。 如果这已经发生,那么它就返回。 这很重要,因为如果您更改图表周期,或进行任何需要 EA 重新加载图表的更改(这种情况经常发生),回放服务的状态将不会更改。 那么,如果服务正在运行,也就是说,如果它暂停了,就按原样继续;如果它正在运行,它将继续发送跳价。

如果是第一次调用,则创建主控制,目前只是播放和暂停按钮。 接下来,我们创建一个全局终端值,该值将用于 EA 和服务之间的通信。 此刻,我们只是创建一个变量,且不为其分配任何值。

=之后,我们必须在屏幕上应用对象。这很重要,因为如果不进行强制更新,EA 将被加载,但服务就会停止,如此令您认为系统崩溃了。 但事实上,我们将等待 MetaTrader 5 为我们更新图表,以便绘制对象,并运行市场回放。

您有没有注意到它是多么容易? 现在我们来看看 DispatchMessage 函数的代码,其在此阶段也非常简单。 下面是它的代码:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        u_Interprocess Info;
                        
        switch (id)
        {
                case CHARTEVENT_OBJECT_CLICK:
                        if (sparam == m_szBtnPlay)
                        {
                                Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
                        }
                        break;
        }
}


我们利用 MetaTrader 5 来管控一切。 我们使用 u_Interprocess 联合来设置全局终端值,从而检查位图按钮的状态。故此,我们调整终端全局变量,如此将其传递给负责创建回放的服务进程。

由此,我们将始终以暂停状态启动重播系统。 一旦 EA 及其所有对象加载到图表上,我们就可以随时播放它,或暂停市场回放。 这会令事情变得更加有趣。

了解 Interprocess.mqh 文件
您也许已经猜到了,将系统切换为使用 EA 替代脚本给回放服务带来了一些变化。 在研究这些变化之前,我们来看一下 Interprocess.mqh 文件。 其当前状态的完整代码如下:
#define def_GlobalVariableReplay "Replay Infos"
//+------------------------------------------------------------------+
union u_Interprocess
{
        double Value;
        struct st_0
        {
                bool isPlay;
                struct st_1
                {
                        char Hours;
                        char Minutes;
                }Time[3];
        }s_Infos;
};


这个简单的定义为我们提供了一个名称,但它不仅仅是任何名称。 这将是全局终端变量的名称,在此阶将用于允许 EA 和服务之间的通信。 但是对于经验较少的用户来说,可能很复杂的部分是联合。

我们看看这个联合实际上代表什么,然后了解它是如何用于在 EA 和服务之间传递信息的。 首先,为了明白其复杂性,您必须知道每种数据类型在使用时的位长(bits)。 为了方便讲述,我建议您参见以下表格:

类型        位长(bits)数量
布尔(bool)        1 位
字符(char)或无符号字符(uchar)        8 位
短整数(short)或无符号短整数(ushort)        16 位
整数(int)或无符号整数(uint)        32 位
长整数(long)或无符号长整数(ulong)        64 位
此表列出了有符号和无符号整数类型,以及位长数量(不要将位长 "bits" 与字节 "bytes" 混淆)。 位(Bit)是表示开或关状态,或二进制中的 1 和 0 的最小信息单位。 字节(Byte)是若干位(bit)的集合。

在查看此表格时,以下想法可能不清楚:在 uchar 类型的变量中,我们将有 8 个 bool 类型的变量。 也就是说,一个 uchar 变量对应于 8 bool 变量的“联合”(这个词不十分准确)。 在代码中,它将如下所示:

union u_00
{
        char info;
        bool bits[8];
}Data;

此联合的长度为 8 位或 1 个字节。 您可以通过在数组中按位写入信息,并选择特定位置来修改信息的内容。 例如,要令 Data.info 等于0x12,您可以执行下面显示的两项操作之一:

Data.info = 0x12;



Data.bits[4] = true;
Data.bits[1] = true;

无论哪种方式,如果 Data.info 变量将所有初始位设置为 0,我们将得到相同的结果。 这就是联合。

现在让我们回到原始代码。 在 64 位系统上找到的最大类型是 long(有符号)或 ulong(无符号)类型。 如果有符号,则可以表示负值。 而无符号只可以表示正数。 那么,在这种情况下,我们会得到这样的东西:



每个方块代表 1 位,名称 “QWORD” 来自汇编,这是所有现代编程语言的母语。 同样的结构发生在另一种类型中 — 浮点数(float)。

浮点数是数值不精确的变量,但仍可用于表示可计算数值。 基本上,有两种类型:

类型        位长(bits)数量
浮点(float)        32 位
双精度(double)        64 位
这类似于上面讨论的整数类型,其中每位表示一个开或关状态。 对于浮点数,我们没有表示逻辑的相同值。 它们遵循略有不同的创建原则,但我们现在不会考虑它。 此处的重要细节是不同的。

当我们查看终端全局变量使用的类型时,我们看到它们只有浮点型,或者更准确地说,是 double:64 位。 问题:具有相同长度的整数类型是什么? 正是您回答的:具有相同 64 位的 long 类型。 当我们把 long 和 double 联合时,我们可以同时代表两个完全不同的东西。

但于此我们遇到一个棘手的问题:您怎么知道该用哪种类型? 为了解决这个问题,我们不用完整类型,而只使用它的片段,并为这些片段分配名称。 这样,我们就得到了联合,您可以在 Interprocess.mqh 文件的代码中看到它。

事实上,我们不打算用到 duble。 试图直接写入手中的数值,以双精度类型创建数值根本不合适或做法太简陋。 取而代之,我们使用命名部分来完成此创建,并且用 0 或 1 表示的调整数值设置对应的位。 之后,我们将 double 数值放在全局终端变量中,另一个进程(在本例中为服务)将获取该数值,并解码它,即可知道该怎么确切去做。

您会看到一切都是遵照非常简单易懂的规则完成的。 如果我们尝试直接创建浮点数,然后理解它们的含义,这将非常困难。

我相信,现在很清楚什么是联合,以及我们将如何使用它。 但请记住:如果您想使用类型为 double 的终端全局变量,它具有 64 位,那么创建的联合同样不能超过 64 位,否则某些信息将丢失。


理解如何创建回放服务
这可能是需要您最多关注的部分,以便明白正在发生的事情。 如果您在不理解的情况下就去做某事,您很可能会搞砸的。 虽然这听起来很简单,但有些细节如果被误解,可能会令您想不明白为什么系统可如描述工作和演示,但您却无法让它在您的工作站上工作。

那么,我们来看看回放服务。 它目前仍然非常紧凑和简单。 其整个代码如下所示:

#property service
#property copyright "Daniel Jose"
#property version   "1.00"
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string    user01 = "WINZ21_202110220900_202110221759"; //File with ticks
//+------------------------------------------------------------------+
C_Replay        Replay;
//+------------------------------------------------------------------+
void OnStart()
{
        ulong t1;
        int delay = 3;
        long id;
        u_Interprocess Info;
        bool bTest = false;
        
        if (!Replay.CreateSymbolReplay(user01)) return;
        id = Replay.ViewReplay();
        Print("Waiting for permission to start replay ...");
        while (!GlobalVariableCheck(def_GlobalVariableReplay)) Sleep(750);
        Print("Replay service started ...");
        t1 = GetTickCount64();
        while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.Value)))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest)     bTest = (Replay.Event_OnTime() > 0); else       t1 = GetTickCount64();
                }else if ((GetTickCount64() - t1) >= (uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < 0) break;
                        t1 = GetTickCount64();
                }
        }
        Replay.CloseReplay();
        Print("Replay service finished ...");
}
//+------------------------------------------------------------------+


如果您取此短代码,并创建 “WINZ21_202110220900_202110221759” 文件用作回放的基础,然后尝试运行它,您将不会看到任何事情发生。 即使您用附件中的文件替换,并尝试据该段代码运行它,也不会发生任何事情。 但这是为什么呢? 原因是 id = Replay.ViewReplay(); 这段代码做了一些您需要搞明白的事情,以便能够真正使用市场回放系统。 无论您做什么:如果您不明白将会发生什么,那就没有什么意义。 但在我们查看 ViewReplay() 中的代码之前,我们先理解上面代码中的数据流。

为了理解它是如何工作的,我们将其分解为更小的部分,并从以下片段开始:

if (!Replay.CreateSymbolReplay(user01)) return;


这一行从指定文件加载交易的跳价数据。 如果加载失败,服务将直接终止。

id = Replay.ViewReplay();


这一行将加载 EA,但我们稍后会更详细地查看这一处,所以我们先继续前进。

while (!GlobalVariableCheck(def_GlobalVariableReplay)) Sleep(750);


上面的行将位于循环当中,等待 EA 加载,或其它东西来创建全局终端变量。 这将作为在服务环境之外运行的进程之间的一种通信形式。

t1 = GetTickCount64();


此行对服务的内部计数器执行第一次捕获。 第一次捕获即可是必需的,也可不是必需的。 通常这是完全没有必要的,因为系统在启用后会立即进入暂停模式。

while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.Value)))


这一处很有意思。 我们这里有两次测试。 如果其中一个失败,回放服务将被终止。 在第一种情况下,我们检查终端中是否存在资产回放窗口。 如果交易者关闭该窗口,回放系统将被终止,因为交易者将不再运行回放。 在第二种情况下,我们测试并同时从终端全局变量中捕获数值。 如果此变量不复存在,服务也将终止。

        u_Interprocess Info;

//...

        if (!Info.s_Infos.isPlay)


在这里,我们检查交易者或回放用户告知的条件。 如果我们处于播放模式,则此测试将失败。 但如果我们处于暂停模式,它将成功。 请注意我们如何使用联合来捕获双精度值内的正确位。 若没有这个联合,这将是不可能做到的。

一旦我们处于暂停模式,我们执行以下行:

if (!bTest) bTest = (Replay.Event_OnTime() > 0); else t1 = GetTickCount64();


此行仅允许将第一次交易跳价发送到资产。 由于稍后将看到的一些原因,这很重要。 一旦此操作完成,任何其它时间回放服务处于“暂停”,我们将捕获计时器的当前值。 的确,这种“暂停”模式并不是指服务实际上已暂停的事实。 它只是没有向回放品种发送跳价,这就是为什么我说它是“暂停”的。

但如果用户或交易者想要开始或恢复市场回放,那么我们输入一行新的代码。 它如下所示:

else if ((GetTickCount64() - t1) >= (uint)(delay))
它将根据跳价之间的延迟值检查是否需要发送新的跳价。 此值在下一行代码中获取。

if ((delay = Replay.Event_OnTime()) < 0) break;


请注意,如果延迟小于 0,回放服务将终止。 这通常发生在最后一次跳价被发送到回放资产的时刻。

这些函数将一直运行到发送最后一次跳价,或回放资产图表关闭。 当这种情况发生时,将执行以下行:

Replay.CloseReplay();


这将永久结束回放。

所有这些代码都非常优美,且易于理解。 但您也许已经注意到,这里有若干处指代的是同一个类,C_Replay。 那么,我们来看看这个类。 它的代码与我们在之前的文章中看到的代码有很多共同之处。 但有一部分值得更多关注。 这正是我们现在要看的。


C_Replay 类的 ViewReplay 为什么如此重要?
这段代码如下所见:

long ViewReplay(void)
{
        m_IdReplay = ChartOpen(def_SymbolReplay, PERIOD_M1);
        ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
        ChartRedraw(m_IdReplay);
        return m_IdReplay;
}

您也许会想:这 4 行中的代码如此重要是允许或阻止创建回放吗?! 尽管这是一段相当简单的代码,但它非常强大。 它是如此强大,以至于即使一切看起来都正确,它也能挡住去路。

我们来看看这一时刻。 我们要做的第一件事是打开一个带有回放资产名称的图表,并将周期设置为 1 分钟。 从前两篇文章中可以看出,我们可以随时更改此时间。

一旦此操作完成后,我们加载一个特定的模板,并将其应用于新打开的图表窗口。 注意这一点很重要,此模板非常具体。 为了创建该模板,如果您已删除它(它将在附件中),则您必须自市场回放系统编译 EA,并将此 EA 应用于任何资产。 然后将此图表另存为模板,并将其命名为 Market Replay,仅此而已。 如果此文件不存在,或者其中不存在 EA,则无论您做了什么,整个系统都将失败。

在某种程度上,如果用指标替代 EA,则可以解决此问题。 在这种情况下,我们将通过 MQL5 调用此指标(理论上)。 但正如我在本文开头所说,我有理由使用 EA 替代指标。 故此,为了以最简单的方式解决加载问题,我们就用一个包含回放系统 EA 的模板。

但这样做导致的简单事实,就是并不能保证太多,因为当加载 EA 时,它会创建一个终端全局变量,告诉服务系统已准备好工作。 然而,控件需要一段时间才能显示。 为了加快速度,我们调用强制更新回放资产图表中的对象。

现在我们返回回放资产图表的 id,因为我们将无法在其它地方执行此操作。 我们需要此信息,以便服务知道图表何时关闭。

C_Replay 类的所有其它函数都很容易理解,因此我们不会在本文中讨论它们。

最近访问 头像模式
举报

评论 使用道具

发新帖
EA交易
您需要登录后才可以评论 登录 | 立即注册

简体中文
繁體中文
English(英语)
日本語(日语)
Deutsch(德语)
Русский язык(俄语)
بالعربية(阿拉伯语)
Türkçe(土耳其语)
Português(葡萄牙语)
ภาษาไทย(泰国语)
한어(朝鲜语/韩语)
Français(法语)