概述
在上一篇文章“开发回放系统 — 市场模拟(第 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 类的所有其它函数都很容易理解,因此我们不会在本文中讨论它们。
|