
开发回放系统 — 市场模拟(第 07 部分):首次改进(II)
概述
在上一篇文章开发回放系统 — 市场模拟(第 06 部分):第一个改进(I)中,我们修复了一些内容,并往我们的回放系统里加入了测试。 这些测试旨在确保尽可能高的稳定性。 与此同时,我们着手为系统创建和使用配置文件。
尽管我们尽最大努力创建一个直观且稳定的回放系统,我们仍会面临一些未解决的难题。 其中一些的解决方案很简单,而另一些则更复杂。
如果您观看了上一篇文章末尾的视频,您可能会注意到系统中仍有一些需要改进的缺陷。 我决定保留这些缺陷可见,如此每个人都明白在开始更密集的使用它之前,还有很多需要改进和纠正的地方。 这并不意味着该系统不能用于实践。
然而,在没有首先消除可能导致系统或平台不稳定的任何细微差别的情况下,我不想添加新功能。 我想有人打算在市场开放时使用回放并练习,等待一笔真实的成交出现,或在平台中使用自动化 EA。
不过,如果回放系统不够稳定,那么在市场开放时,如果发生一笔真实交易,保持其运行也许是不切实际的。 此警告是由于回放系统可能会导致故障,或干扰平台其它功能的正常运行。
因此,随着我们继续努力提高市场回放的稳定性和便利性,我们将在代码中引入一些额外的资源,以便进一步提升系统。
确保控制指示器保持在图表上
第一个我们将实现的改进是控制指标。 目前,当回放开始时,会加载一个模板。 此模板文件必须包含控制回放服务的指标。 到目前为止,一切都很好:如果该指标不存在,则无法启动回放服务进行操作。 因此,该指标对服务至关重要。
不过,下载后直到现在,我们无法保证该指标能一直保留在图表上。 现在情况会改变,我们将确保它会驻留在图表上。 如果由于某种原因指标被删除了,则必须立即停止回放服务。
但我们如何能做到这一点呢? 我们如何让指标保持在图表上,或者我们如何检查它是否真的存在? 确有若干种方法,但在我看来,最简单和最优雅的一种是由 MetaTrader 5 平台本身利用 MQL5 语言来检查。
常规而言,指标中没有特定事件,但有一个对我们特别有用:DeInit 事件。
如果发生某些事情,则触发相应事件,此刻若指标需要关闭,就会立即触发 OnInit 事件。 因此,在调用 OnDeInit 函数时,我们能否告之回放服务指标已被删除? 这是可以的,但有一点关键点。 OnDeInit 事件不仅在指标被删除或图表关闭时被调用。
当图表周期更改,或指标参数更改时,也会触发它。 所以看起来事情又变得复杂了。 不,如果我们查看 OnDeInit 事件的文档,我们将看到可以使用逆初始化原因代码。
查看这些代码,我们注意到它们当中有两段代码对于指示控制指标已从图表中删除非常有用。 因此,我们来创建以下代码:
void OnDeinit(const int reason) { switch (reason) { case REASON_REMOVE: case REASON_CHARTCLOSE: GlobalVariableDel(def_GlobalVariableReplay); break; } }
于此,我们检查从图表中删除指标是否导致 DeInit 事件触发(以及相应的 OnDeInit 函数)。 在这种情况下,我们必须通知回放服务,该指标已不存在于图表之上,并且必须立即停止该服务。
为此,您需要从终端中删除全局变量,其为控制指标和回放服务之间的链接。 一旦删除此变量后,回放服务就明白活动已结束,并关闭。
如果您查看服务代码,您会注意到当它关闭时,回放品种图表也会关闭。 当服务执行以下代码时,的确会发生这种情况:
void CloseReplay(void) { ArrayFree(m_Ticks.Info); ChartClose(m_IdReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); GlobalVariableDel(def_GlobalVariableReplay); }
故此,当您删除控制指标时,图表将与指标一起关闭,因为回放服务会强制关闭它。 不过,该服务可能没时间关闭回放品种图表。 在这种情况下,我们需要确保该图表被关闭,即使品种仍保留在市场观察窗口之中,并且出于某种原因,尽管进行了所有尝试,回放服务仍无法删除它。
为此,我们在控制指标的 OnDeInit 函数中再添加一行。
void OnDeinit(const int reason) { switch (reason) { case REASON_REMOVE: case REASON_CHARTCLOSE: GlobalVariableDel(def_GlobalVariableReplay); ChartClose(ChartID()); break; } }
现在发生以下情况:一旦控制指标因任何原因从图表中被删除,但回放服务无法关闭图表,指标本身将尝试关闭它。 这似乎有点违反直觉,但我希望图表能像回放服务一样,释放给平台,并且在发生任何故障或错误时不会造成任何不便。
通过此实现,我们至少可以保证,如果从图表中删除控制指标,服务将停止,且图表被关闭。 不过,我们还有另一个与指标相关的问题。
防止控制指标被删除
这是一个相当严重的问题,因为指标可能会遗留在图表上,但其组成元素将被简单地删除或销毁,这令指标不能再正确使用。
幸运的是,这种情况很容易修复。 不过,这一刻需要特别小心,如此它就不会在将来变成问题的根源。 若只是防止指标被破坏,或者它的元素和对象被移除,我们可能创建一个无法控制的怪物,这会导致很多麻烦。 为了解决这个问题,我们将拦截并处理图表上的对象销毁事件。 我们来看看这在实践中是如何完成的。
我们首先将以下代码行添加到 C_Control 类:
void Init(const bool state = false) { if (m_szBtnPlay != NULL) return; m_id = ChartID(); ChartSetInteger(m_id, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(m_id, CHART_EVENT_OBJECT_DELETE, true); CreateBtnPlayPause(m_id, state); GlobalVariableTemp(def_GlobalVariableReplay); if (!state) CreteCtrlSlider(); ChartRedraw(); }
通过添加这行代码,我们要求 MetaTrader 5 从屏幕中删除图表对象时向我们发送一个事件。 仅仅满足这个条件并不能保证对象不会被删除,但它至少保证了 MetaTrader 5 平台会在发生这种情况时通知我们。
为了确保在删除 C_Control 类时删除对象,我们必须告诉 MetaTrader 5 何时不发送对象删除事件。 所用此类函数的要点之一如下所示:
~C_Controls() { m_id = (m_id > 0? m_id : ChartID()); ChartSetInteger(m_id, CHART_EVENT_OBJECT_DELETE, false); ObjectsDeleteAll(m_id, def_PrefixObjectName); }
以这种方式,我们将告之 MetaTrader 5,当一个对象从图表中删除时,我们不希望它向我们发送事件,我们可以删除必要的对象,而不会出现进一步问题。
然而,事情并没有那么简单,这里有一个潜在的问题。 我们来看以下代码:
void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { u_Interprocess Info; static int six =-1, sps; int x, y, px1, px2; switch (id) { case CHARTEVENT_OBJECT_CLICK: if (sparam == m_szBtnPlay) { Info.s_Infos.isPlay =(bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE); if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else { ObjectsDeleteAll(m_id, def_PrefixObjectName + "Slider"); m_Slider.szBtnPin = NULL; } Info.s_Infos.iPosShift = m_Slider.posPinSlider; GlobalVariableSet(def_GlobalVariableReplay, Info.Value); ChartRedraw(); }else if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1); else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1); break; // ... The rest of the code....
这一行会删除位置控制条,并触发对象删除事件。
您也许认为我们可以简单地关闭事件,然后删除控制面板,并重新打开它。 这是对的,但请记住,随着代码量的增加,这些打开和关闭操作变得比表面看要普遍得多。 此外,还有一件事:为了正确表示,您需要按特定顺序排列对象。
故此,仅打开和关闭删除事件,并不能保证该事件得以正确处理。 我们需要创建一个更优雅和强大的解决方案,以便维护对象的正确顺序,从而它们的呈现始终相同,并且用户不会注意到定位系统的差异。
最简单的解决方案是创建一个函数,该函数关闭 “delete” 事件,删除同一链中的对象,然后重新打开删除事件。 这可以使用以下代码轻松实现,该代码将在控制栏中执行此任务。
inline void RemoveCtrlSlider(void) { ChartSetInteger(m_id, CHART_EVENT_OBJECT_DELETE, false); ObjectsDeleteAll(m_id, def_NameObjectsSlider); ChartSetInteger(m_id, CHART_EVENT_OBJECT_DELETE, true); }
现在,每次我们需要删除控制面板时,仅需调用这个函数,就可获得所需的结果。
尽管这看起来微不足道,但在当前状态下,此过程不会仅用到一次,而是在同一函数中用到两次,如下所示:
void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { u_Interprocess Info; static int six =-1, sps; int x, y, px1, px2; switch (id) { case CHARTEVENT_OBJECT_DELETE: if(StringSubstr(sparam, 0, StringLen(def_PrefixObjectName)) == def_PrefixObjectName) { if(StringSubstr(sparam, 0, StringLen(def_NameObjectsSlider)) == def_NameObjectsSlider) { RemoveCtrlSlider(); CreteCtrlSlider(); }else { Info.Value = GlobalVariableGet(def_GlobalVariableReplay); CreateBtnPlayPause(Info.s_Infos.isPlay); } ChartRedraw(); } break; case CHARTEVENT_OBJECT_CLICK: if (sparam == m_szBtnPlay) { Info.s_Infos.isPlay =(bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE); if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else { RemoveCtrlSlider(); m_Slider.szBtnPin = NULL; } Info.s_Infos.iPosShift = m_Slider.posPinSlider; GlobalVariableSet(def_GlobalVariableReplay, Info.Value); ChartRedraw(); }else if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1); else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1); break; case CHARTEVENT_MOUSE_MOVE: x =(int)lparam; y =(int)dparam; px1 = m_Slider.posPinSlider + def_MinPosXPin - 14; px2 = m_Slider.posPinSlider + def_MinPosXPin + 14; if ((((uint)sparam & 0x01) == 1) && (m_Slider.szBtnPin != NULL)) { if ((y >= (m_Slider.posY - 14)) && (y <= (m_Slider.posY + 14)) && (x >= px1) && (x <= px2) && (six ==-1)) { 6 = x; sps = m_Slider.posPinSlider; ChartSetInteger(m_id, CHART_MOUSE_SCROLL, false); } if (six > 0) PositionPinSlider(sps + x - six); }else if (6 > 0) { 6 =-1; ChartSetInteger(m_id, CHART_MOUSE_SCROLL, true); } break; } }
我们抵近看看负责处理对象删除事件的部分。 当我们告诉 MetaTrader 5 平台,我们希望从图表中删除对象时接收事件,那它就会为每个删除的对象生成一个删除事件。 然后,我们捕获此事件,就可以检查哪个对象被删除了。
一个重要的细节是,您看到的不是哪个对象将被删除,而是真实被删除的那个对象。 在我们的例子中,我们检查它是否是控制指标所用的指标之一。 如果是的话,我们将复查它是否为控制面板上的对象之一,或者它是否为控制按钮。 如果它是控制面板中的对象之一,则该面板将被完全删除,并立即再次创建。
我们不需要向这个创建函数通知任何东西,因为它本身会完成所有工作。 现在,当涉及到控制按钮时,我们的情况略有不同。 在这种情况下,我们必须读取终端的全局变量,找出按钮的当前状态,然后才能请求创建按钮。
最后,我们将所有对象强制放置在图表上,如此用户甚至不会注意到它们曾被删除。
我们这样做是为了确保一切都到位。 现在,我们看一下对于回放服务的操作也很重要的其它内容。
只有一个回放图表
在使用自动打开图表的系统时,经常会发生打开相同品种图表的情况,一段时间后,我们不再掌控我们到底在应对什么。
为了避免这种情况,我实现了一个小测试,解决了回放系统为了重播市场目的而不断重复打开一个又一个图表的问题。 此函数的存在也保证了全局终端变量中所包含数值的某种稳定性。
如果我们有若干个图表反映相同的想法,在这种情况下是市场回放,那么我们可能会发现其中一个持有控制指标创建的指定值,而其它那些都是完全不同的值。 虽然这个问题还没有完全解决,简单的事实就是,我们不再享有多个图表同时引用同一品种带来的诸多益处。
下面的代码展示的方式,确保只为给定金融产品打开一个图表:
long ViewReplay(ENUM_TIMEFRAMES arg1) { if ((m_IdReplay = ChartFirst()) > 0) do { if(ChartSymbol(m_IdReplay) == def_SymbolReplay) ChartClose(m_IdReplay); }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0); m_IdReplay = ChartOpen(def_SymbolReplay, arg1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); ChartRedraw(m_IdReplay); return m_IdReplay; }
于此,我们检查 MetaTrader 5 平台终端中是否有打开的图表。 如果有,那么我们用它作为起点来检查哪个交易品种已打开。 如果品种市场回放所用品种,那么我们就关闭此图表。
问题是,如果回放品种图表已打开,那么我们有两种选择:第一种是关闭图表,这正是我们所做的。 第二种选择是结束循环,但在第二种情况下,可能会发生同一资产的多个图表均处于打开状态的情况。 因此,我更喜欢关闭所有已打开的图表;我们也将这样做,直到检查最后一个图表。 结果就是,我们没有了打开的市场回放图表。
接下来我们需要再次打开包含市场回放系统的图表,应用模板以便可以使用控制指标,强制显示图表,并返回打开图表的索引。
但并未阻止我们在系统加载后为回放品种打开新图表。 我们可以向服务添加一个额外的测试,以便在整个回放期间只有一个图表保持打开状态。 但我知道有些交易者喜欢同时使用同一品种的多个图表。 每个图表都将使用自己的时间间隔。
出于这个原因,我们不会添加这个额外的测试,但会做其它事情。 我们不允许在服务打开的图表以外的任何图表上存在和操作控制指标。 好吧,我们可以通过尝试用不同的图表替换原始图表来结束原始图表上的指标。 但是图表将关闭,回放服务将停止,如此来阻止进行更改。
在下一个主题中,我们将查看如何确保控制指标不会在原始图表以外的任何其它图表上打开。
每个会话只有一个控制指标
这部分非常有趣,在某些情况下可以帮到您。 我们看看如何确保指标在一个 MetaTrader 5 工作会话中只属于一个图表。
为了理解如何执行此操作,我们来查看以下代码:
int OnInit() { u_Interprocess Info; IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay"); if(GlobalVariableCheck(def_GlobalVariableReplay)) Info.Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.Value = 0; Control.Init(Info.s_Infos.isPlay); return INIT_SUCCEEDED; }
此代码将检查是否存在全局终端变量。 如果存在这样的变量,我们将捕获它,以供后期所用。 如果没有该变量,那么我们初始化它。
但有一个重要的细节:无论是在图表上还是在更新指标参数时,每当出现这种事情,都会调用 OnInit 函数。 在这种情况下,指标不包含任何参数,也不会接收它们。 因此,只在每次图表时间间隔变化时,我们才会得到发生的图表事件,即,如果我们从 5 分钟变为 4 分钟,将调用 OnInit。 在这种情况下,如果我们只针对存在全局终端变量的情况阻止指标的初始化,那么我们就会遇到问题。 原因是图表将被关闭,这意味着服务将被停止。 这很难,不是吗?
但是我们将采取的解决方案非常简单,同时又非常优雅。 我们将利用全局终端变量来了解控制指标是否已存在于任意图表上。 如果存在,只要它存在于当前 MetaTrader 5 会话中的任意已打开图表上,就不能将其再次放置在另一个图表上。
为了实现这一点,我们需要编辑用于进程之间通信的代码。
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #define def_GlobalVariableReplay "Replay Infos". //+------------------------------------------------------------------+ #define def_MaxPosSlider 400 //+------------------------------------------------------------------+ union u_Interprocess { double Value; struct st_0 { bool isPlay; // Specifies in which mode we are - Play or Pause ... bool IsUsing; // Specifies if the indicator is running or not ... int iPosShift; // Value from 0 to 400 ... }s_Infos; };
请记住,我们可以往结构中添加内部变量,只要它不超过 8 字节长度限制,这是双精度变量占用的内存尺寸。 但是由于布尔类型只需用到 1 位即可存在,故此 isPlay 变量所用的字节中还剩下 7 个空闲位,我们可以轻松地再添加 7 个布尔数字。 因此,我们将使用这 7 个自由位中的一个来找出任何图表上是否存在控制指标。
注意:尽管此机制足以胜任,但存在一个问题。 但我们暂时不会谈论这个。 我们会在将来需要更改结构时在另一篇文章中研究这个问题。
所以,您也许认为这就足够了。 但是我们需要在代码中添加一些东西。 不过,我们不会担心服务代码,但只是更改指标代码,如此添加的变量就会对我们有实际效用。
我们需要做的第一件事是在指标代码中添加一些额外的行。
void Init(const bool state = false) { u_Interprocess Info; if (m_szBtnPlay != NULL) return; m_id = ChartID(); ChartSetInteger(m_id, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(m_id, CHART_EVENT_OBJECT_DELETE, true); CreateBtnPlayPause(state); GlobalVariableTemp(def_GlobalVariableReplay); if (!state) CreteCtrlSlider(); ChartRedraw(); Info.Value = GlobalVariableGet(def_GlobalVariableReplay); Info.s_Infos.IsUsing = true; GlobalVariableSet(def_GlobalVariableReplay, Info.Value); }
此处,我们在全局终端变量中通知并记录控制指标已创建。 但为什么我们需要之前的调用来创建全局终端变量呢? 我们不能跳过它吗? 这第一次调用实际上是用来通知 MetaTrader 5 平台全局变量是临时变量,不必维护。 即使您要求平台保存全局终端变量的数据,这些被视为临时变量的值也不会保存,并会丢失。
这就是我们所需要的,因为如果我们需要保存,然后重置全局终端变量,那么有一个报告控制指标存在的变量是不符实际的,因实际上控制指标并不存在。 出于这个原因,我们必须以这种方式来做。
您应该小心这件事。 因为当平台在图表上重新定位指标时,全局终端变量的值可能会有所不同,因为我们已经在回放中有些提前。 如果我们没有这一行,系统将从头开始回放系统。
我们还有更多的事情要做。
case CHARTEVENT_OBJECT_CLICK: if (sparam == m_szBtnPlay) { Info.s_Infos.isPlay =(bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE); if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else { RemoveCtrlSlider(); m_Slider.szBtnPin = NULL; } Info.s_Infos.IsUsing = true; Info.s_Infos.iPosShift = m_Slider.posPinSlider; GlobalVariableSet(def_GlobalVariableReplay, Info.Value); ChartRedraw(); }else if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1); else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1); break;
每次按下暂停/播放按钮时,全局终端变量的值都会更改。 但遵照前面的代码,当单击按钮时,保存的值将不再包含图表上存在控制指标的指示。 因此,我们需要添加一行代码。 有了它,我们就有一个正确的指示,因为从指定位置来播放,反之亦然,而不会产生虚假的指示。
与 C_Replay 类相关的部分已经完成,但我们还有一点工作要做。 简单地创建一个指示,并不能保证除了它存在的事实之外的任何事情。 我们来继续讨论指标代码。 在此,您应更加小心,如此才能一切正常,并且不会变成行为奇怪的东西。
那么,我们来关注细节。 您应该注意的第一件事是 OnInit 代码:
int OnInit() { #define def_ShortName "Market Replay" u_Interprocess Info; IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); if(GlobalVariableCheck(def_GlobalVariableReplay)) { Info.Value = GlobalVariableGet(def_GlobalVariableReplay); if (Info.s_Infos.IsUsing) { ChartIndicatorDelete(ChartID(), 0, def_ShortName); return INIT_FAILED; } } else Info.Value = 0; Control.Init(Info.s_Infos.isPlay); return INIT_SUCCEEDED; #undef def_ShortName }
出于实际原因,我们在这里创建了一个定义来指定指标的名称:我们可以在窗口中查看图表上的实际指标,并允许我们按这个名称从窗口加载的指标列表中删除该指标。 即使那些不能可视化的也会显示在此窗口中。 我们不希望任何无用的指标留在那里。
因此,从图表中删除指标后,我们需要查看它是否还存在于任何图表上。 为此,我们检查在终端全局变量中创建的值。 这令得检查变得非常简单和高效。 还有其它方法可以执行此检查,但由于我们使用的是全局终端变量,因此检查它更容易。
该功能的其余部分以相同的方式继续,但不再可能在单个 MetaTrader 5 会话中向多个图表添加控制指标。 此处和附加的代码中,我们没有添加指标已存在于某个图表上的警告,但您可以在函数返回初始化错误之前添加此警告。
这似乎足够了,但尚有其它问题需要我们解决。 应该记住,每次 MetaTrader 5 收到更改时间帧的请求时(这可能是平台上最常见的事件),所有指标,就像许多其它事情一样,都将被删除,然后重置。
现在考虑以下几点。 如果指标通过全局变量通知有任何图表正在执行副本,并且您更改了此特定图表的时间帧,则该指标将被删除。 但当 MetaTrader 5 平台试图将指标恢复到图表时,它将无法放置在图表上。 其原因正是我们在 OnInit 函数的代码中看到的。 我们需要以某种方式编辑全局终端变量,如此它不再报告控制指标的存在。
有相当奇特的方式可以解决这个问题,但是,同样,MetaTrader 5 平台和 MQL5 语言提供了相当简单的方法来解决这个问题。 我们查看以下代码:
void OnDeinit(const int reason) { u_Interprocess Info; switch (reason) { case REASON_CHARTCHANGE: if(GlobalVariableCheck(def_GlobalVariableReplay)) { Info.Value = GlobalVariableGet(def_GlobalVariableReplay); Info.s_Infos.IsUsing = false; GlobalVariableSet(def_GlobalVariableReplay, Info.Value); } break; case REASON_REMOVE: case REASON_CHARTCLOSE: GlobalVariableDel(def_GlobalVariableReplay); ChartClose(ChartID()); break; } }
正如我们所记,删除指标时,会生成 DeInit 事件,该事件调用 OnDeInit 函数。 此函数接收的参数值,能指示调用原因。 这是我们将采用的值。
此值可以在逆初始化原因代码中看到。在此,我们看到的 REASON_CHARTCHANGE 指示图表周期已更改。 所以我们做一个检查 - 认真检查总是好的。 永远不要想当然或假设,但始终检查,是否存在具有预期名称的终端全局变量。如果为真,我们将捕获变量的值。 由于服务可能正在做某事,并且我们不想打扰它,那么我们在此处修改信息,表明控制指标将不再出现在图表上。 完成此操作后,我们将信息写回全局终端变量。
在此,我必须警告这个系统可能存在的缺陷。 尽管出现问题的可能性很低,但您应该始终知道该方法存在缺陷,从而为可能出现的问题做好准备。
这里的问题是读取和写入变量之间会有很小的时差。 尽管它很短暂,但若服务在指标之前将数值写入全局终端变量时,它就存在。 发生此类事件时,服务在访问全局终端变量时所期望的值将与变量中的实际值不同。
有很多方法可以绕过这个缺陷,但在这个与市场回放配套工作的系统中,这并不重要。 故此,我们可以忽略此缺陷。 然而,如果您想在更复杂的事情中使用相同的机制,其中存储的数值至关重要,那么我建议您查找有关如何锁定和解锁共享内存进行读取和写入的更多信息。 好吧,终端全局变量正是共享内存。
从下面的视频中,您可以了解一些已修复,以及尚待修复的内容。 现在事情变得越来越严谨。
结束语
尽管此处讲述的系统似乎是消除因滥用控制指标引起的故障的理想选择,但它仍然不是一个真正有效的解决方案,因为它只能避免我们实际可能遇到的某些类型的问题。
我想,看完视频后,您会注意到我们必须解决另一个问题,这个问题比表面上看要复杂得多。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10784
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.

