English Русский Español Deutsch 日本語 Português
preview
开发回放系统(第 65 部分):玩转服务(六)

开发回放系统(第 65 部分):玩转服务(六)

MetaTrader 5示例 |
226 0
Daniel Jose
Daniel Jose

概述

在上一篇文章“开发回放系统(第 64 部分):玩转服务(五) “中,我们修复了回放/模拟应用程序中的两个错误。然而,并非所有问题都得到了解决。至少还达不到我们可以在本文中推进新进展的程度。仍有一些小问题还在影响系统。当我们使用全局终端变量时,这些问题并不存在。但由于我们已经放弃了这种方法,并采用了新技术和方法来使回放/模拟应用程序发挥作用,所以我们现在需要适应并构建一个新的实现。话虽如此,亲爱的读者,您可能已经注意到,我们并不是从零开始。事实上,我们正在调整和改进现有代码,以确保之前使用全局终端变量完成的工作不会完全丢失。

有了这个,我们现在几乎处于与以前相同的功能水平。然而,为了达到同样的目标,我们仍然需要敲定更多的细节。我将在本文中尝试这样做,因为剩下的问题相对简单。这与我们之前解决的内存崩溃错误不同。这个问题相当复杂,需要详细解释为什么会出现错误,即使代码看起来完全正确。仅仅显示需要添加的代码行是不够的,许多读者会感到困惑。或者,如果我只是在没有任何解释的情况下修复了代码,那同样会令人失望。你永远不会遇到这样的问题,可能会产生一种虚假的安全感。然后,当类似的问题出现时,如果没有明确的指导来源,你会感到沮丧。更糟糕的是,你可能会开始怀疑自己作为一名专业人士的能力。我想避免这种情况。即使是经验丰富的专业人士也会犯错。虽然你可能不会总是看到它们,但它们确实会发生。使他们与众不同的是他们快速识别和解决这些问题的能力。这就是为什么我希望每个有抱负的开发人员都能成长为真正的专业人士。而且他们并不是普通的专业人士,而是各自领域的杰出人士。考虑到这一点,让我们解决剩下的第一个问题。


添加快进功能(基本模型)

此功能在过去就存在,并在我们依赖全局终端变量的时期实现。由于我们不再使用这些变量,我们需要调整代码以重新引入快进功能。我将保留我们之前使用的快进逻辑。因此,您将能够更容易地理解如何调整遗留实现以适应新系统。

首先,我们需要对上一篇文章中介绍的代码进行一个小修改。此更改可确保控制指标正常运行。您可以看到以下变化:

35. //+------------------------------------------------------------------+
36. inline void UpdateIndicatorControl(void)
37.          {
38.             double Buff[];
39.                                  
40.             if (m_IndControl.Handle == INVALID_HANDLE) return;
41.             if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
42.             {
43.                if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
44.                   m_IndControl.Memory.dValue = Buff[0];
45.                if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState)
46.                   if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)
47.                      m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
48.             }else if (m_IndControl.Mode == C_Controls::ePause)
49.             {
50.                m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
51.                m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
52.                m_IndControl.Memory._8b[7] = 'D';
53.                m_IndControl.Memory._8b[6] = 'M';
54.                EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
55.             }
56.          }
57. //+------------------------------------------------------------------+

来自文件 C_Replay.mqh 的代码片段

更改,或者更确切地说是添加,是专门在第 48 行进行的。但为什么需要进行这种更改呢?原因在于 LoopEventOnTime 函数内的代码。等等,这听起来令人困惑。如果问题源于 LoopEventOnTime 内部发生的事情,为什么要对 UpdateIndicatorControl 程序进行更改?这没有道理。事实上,如果 LoopEventOnTime 函数不是通过发送和接收消息来读取和写入控制指标的话,那么它就没有意义了。如果第 48 行没有条件检查,当你试图快进然后立即按下播放键时,会发生一些异常情况。这甚至是在我们实现实际的快进逻辑之前。

如果您快进然后按播放,您将无法向服务发送暂停命令。什么?这听起来很荒谬。当然,按下暂停按钮会向服务发送相应的更新,对吗?它确实会发送更新,指示系统暂停。然而,效果并不是立竿见影的。你为什么这么认为?原因就在第 41 行。问题在于控制指标的缓冲区和所引用的内存空间不同步。如果您启动服务,按下播放键,然后快进时间,似乎不会出现任何问题。但是,如果您暂停服务并在缺少第 48 行检查的情况下尝试快进,则控制指标的锁定杆将随着快进一起移动。这将会阻止用户手动调整位置。

现在,如果您启动回放/模拟服务,向前移动一个位置,然后点击播放,您将无法使用暂停按钮暂停该服务。仅当第 41 行的条件变为 true 并且缓冲区指示暂停模式处于活动状态时,它才会停止。这可能需要相当长的时间。也许这个解释似乎有点纠结,但这是因为我们需要考虑三种不同的情况。由于 LoopEventOnTime 在回放/模拟过程的正常执行过程中不断读取并向控制指标发送消息,因此每个过程都有其自身的挑战。

然而,只需添加一个在第 48 行实现的条件测试,如代码片段所示,所有这些问题都得到了解决。我们消除了与 LoopEventOnTime 函数相关的问题。这使我们能够完全专注于使快进功能按预期工作。

实现快进实际上并不是一项复杂的任务。事实上,我们只需将以下代码片段添加到 C_Replay.mqh 文件中即可实现。

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. #include "C_ConfigService.mqh"
005. #include "C_Controls.mqh"
006. //+------------------------------------------------------------------+
007. #define def_IndicatorControl   "Indicators\\Market Replay.ex5"
008. #resource "\\" + def_IndicatorControl
009. //+------------------------------------------------------------------+
010. #define def_CheckLoopService ((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != ""))
011. //+------------------------------------------------------------------+
012. #define def_ShortNameIndControl "Market Replay Control"
013. //+------------------------------------------------------------------+
014. class C_Replay : public C_ConfigService
015. {
016.    private   :

              ...

035. //+------------------------------------------------------------------+
036. inline void UpdateIndicatorControl(void)
037.          {
038.             double Buff[];
039.                                  
040.             if (m_IndControl.Handle == INVALID_HANDLE) return;
041.             if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
042.             {
043.                if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
044.                   m_IndControl.Memory.dValue = Buff[0];
045.                if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState)
046.                   if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)
047.                      m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
048.             }else if (m_IndControl.Mode == C_Controls::ePause)
049.             {
050.                m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
051.                m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
052.                m_IndControl.Memory._8b[7] = 'D';
053.                m_IndControl.Memory._8b[6] = 'M';
054.                EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
055.             }
056.          }
057. //+------------------------------------------------------------------+

              ...

068. //+------------------------------------------------------------------+
069. inline void CreateBarInReplay(bool bViewTick)
070.          {
071.             bool    bNew;
072.             double dSpread;
073.             int    iRand = rand();
074. 
075.             if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew))
076.             {
077.                m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay];
078.                if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
079.                {                  
080.                   dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
081.                   if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
082.                   {
083.                      m_Infos.tick[0].ask = m_Infos.tick[0].last;
084.                      m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
085.                   }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
086.                   {
087.                      m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
088.                      m_Infos.tick[0].bid = m_Infos.tick[0].last;
089.                   }
090.                }
091.                if (bViewTick) CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
092.                CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
093.             }
094.             m_Infos.CountReplay++;
095.          }
096. //+------------------------------------------------------------------+

              ...

123. //+------------------------------------------------------------------+
124.       void AdjustPositionToReplay(void)
125.          {
126.             int nPos;
127.             
128.             if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxPosSlider * 1.0) / m_MemoryData.nTicks)) return;
129.             nPos = (int)(m_MemoryData.nTicks * ((m_IndControl.Position * 1.0) / (def_MaxPosSlider + 1)));
130.             while ((nPos > m_Infos.CountReplay) && def_CheckLoopService)
131.                CreateBarInReplay(false);
132.          }
133. //+------------------------------------------------------------------+
134.    public   :
135. //+------------------------------------------------------------------+

              ...

200. //+------------------------------------------------------------------+
201.       bool LoopEventOnTime(void)
202.          {         
203.             int iPos;
204. 
205.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
206.             {
207.                UpdateIndicatorControl();
208.                Sleep(200);
209.             }
210.             m_MemoryData = GetInfoTicks();
211.             AdjustPositionToReplay();
212.             iPos = 0;
213.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
214.             {
215.                if (m_IndControl.Mode == C_Controls::ePause) return true;
216.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
217.                CreateBarInReplay(true);
218.                while ((iPos > 200) && (def_CheckLoopService))
219.                {
220.                   Sleep(195);
221.                   iPos -= 200;
222.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxPosSlider) / m_MemoryData.nTicks);
223.                   UpdateIndicatorControl();
224.                }
225.             }
226. 
227.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
228.          }
229. //+------------------------------------------------------------------+
230. };
231. //+------------------------------------------------------------------+
232. #undef macroRemoveSec
233. #undef def_SymbolReplay
234. #undef def_CheckLoopService
235. //+------------------------------------------------------------------+

来自文件 C_Replay.mqh 的代码片段

如上文 C_Replay.mqh 文件中的代码片段所示,我们可以清楚地看到实现基本快进功能所需的一切。我说基本是因为还有一些小问题需要解释。然而,理解这种更简单的方法对你来说很重要,因为它为我们即将构建的更完整的实现奠定了基础。请注意前面提到的第 48 行。现在,看一下第 207 行。如果第 48 行的条件不存在,这一行就会导致控制指标出现问题。您可以通过禁用第 48 行和第 211 行的检查来观察问题。但由于控制指标在我们当前版本中已经正常运行,我们将保持原样。让我们重点了解快进在这个基本模型中是如何工作的。

当服务代码调用 LoopEventOnTime 函数时,我们处于暂停模式。此时,从第 205 行开始到第 209 行结束的循环持续运行,允许用户调整控制指标的位置。这是我们所做的。一旦用户在回放/模拟器界面中按下播放按钮,我们就会在第 210 行捕获资产数据以便更快地访问。然后,在第 211 行,我们调用在第 124 行定义的过程。这是负责从当前位置快进到用户指定位置的过程。

在第 128 行,我们检查请求的位置是否与当前位置匹配。如果是,则过程结束,并开始执行第 213 行和第 225 行之间的循环。如果期望位置在前方,我们就在第 129 行计算目标偏移量。到目前为止还没有什么特别神奇的事情,但真正的技巧发生在第 130 行,我们进入一个循环,反复触发第 131 行。此行代码调用负责生成柱形的程序。该过程尽可能快地运行,直到位置计数器与计算的目标相匹配。但是,您会注意到第 131 行调用的过程将执行第 69 行和第 95 行之间的代码。请注意,我们向该柱形生成程序传递了一个错误参数。因此,第 91 行的检查会阻止调用 CustomTicksAdd 函数。这意味着没有分时报价被推送到交易品种。然而,在第 92 行,柱形仍然按照构建的方式逐一呈现在图表上。

总体而言,该过程运行良好,除了两个在快进执行期间引入明显延迟的特定方面。如果您作为用户请求向前跳过足够大的内容,您实际上会看到图表上绘制的柱形。除了过程调用本身之外,这种延迟的主要来源是第 75 行和第 92 行。虽然后者的开销相对较小,可以忽略不计。特别是第 75 行,是造成延迟的一个重要因素。

那么,这就是实现快进的基本方法。但还有一种更快的替代方法。如果您想看到生成的柱形,这种更简单的方法就很好了。通过修改 C_Replay.mqh 文件并包含上面讨论的代码,基本的快进功能已经可以实现。这取决于您是使用这种方法还是选择更高级、速度更快的版本,我们将在接下来进行探索。为了将信息分开,让我们转到一个新的部分。


添加快进功能(动态模型)

如果您仔细查看代码,您会注意到 C_FileTicks 类在报价加载期间已经生成了一分钟的柱形。那么为什么要浪费时间重建已经创建的东西呢?相反,我们将利用这些预先构建的柱形来使我们尽可能接近目标点。如果需要,我们可以继续快进到精确计算的位置。这使得快进过程感觉几乎是瞬间完成的。

当然,并非所有东西都是完美无缺的。为了达到所需的效率水平,我们需要引入某种索引或引用链接来进行快速查找。幸运的是,我们可以重新利用在回放/模拟系统环境中不是特别有用的数据结构部分:MqlRates 结构中的 Spread 字段。要了解这些变化,请看以下代码片段:

126. //+------------------------------------------------------------------+
127.       bool BarsToTicks(const string szFileNameCSV, int MaxTickVolume)
128.          {
129.             C_FileBars     *pFileBars;
130.             C_Simulation   *pSimulator = NULL;
131.             int            iMem = m_Ticks.nTicks,
132.                            iRet = -1;
133.             MqlRates       rate[1];
134.             MqlTick        local[];
135.             bool           bInit = false;
136.             
137.             pFileBars = new C_FileBars(szFileNameCSV);
138.             ArrayResize(local, def_MaxSizeArray);
139.             Print("Converting bars to ticks. Please wait...");
140.             while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
141.             {
142.                if (!bInit)
143.                {
144.                   m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX);
145.                   pSimulator = new C_Simulation(SetSymbolInfos());
146.                   bInit = true;
147.                }
148.                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
149.                m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
150.                if (pSimulator == NULL) iRet = -1; else iRet = (*pSimulator).Simulation(rate[0], local, MaxTickVolume);
151.                if (iRet < 0) break;
152.                rate[0].spread = m_Ticks.nTicks;
153.                for (int c0 = 0; c0 <= iRet; c0++)
154.                {
155.                   ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
156.                   m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
157.                }
158.                m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
159.             }
160.             ArrayFree(local);
161.             delete pFileBars;
162.             delete pSimulator;
163.             m_Ticks.bTickReal = false;
164.             
165.             return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > 0));
166.          }
167. //+------------------------------------------------------------------+
168.       datetime LoadTicks(const string szFileNameCSV, const bool ToReplay, const int MaxTickVolume)
169.          {
170.             int      MemNRates,
171.                      MemNTicks,
172.                      nDigits,
173.                      nShift;
174.             datetime dtRet = TimeCurrent();
175.             MqlRates RatesLocal[],
176.                      rate;
177.             MqlTick  TicksLocal[];
178.             bool     bNew;
179.             
180.             MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
181.             nShift = MemNTicks = m_Ticks.nTicks;
182.             if (!Open(szFileNameCSV)) return 0;
183.             if (!ReadAllsTicks()) return 0;         
184.             rate.time = 0;
185.             nDigits = SetSymbolInfos(); 
186.             m_Ticks.bTickReal = true;
187.             for (int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++)
188.             {
189.                if (nShift != c0) m_Ticks.Info[nShift] = m_Ticks.Info[c0];
190.                if (!BuildBar1Min(c0, rate, bNew)) continue;
191.                if (bNew)
192.                {
193.                   if ((m_Ticks.nRate >= 0) && (ToReplay)) if (m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume)
194.                   {
195.                      nShift = MemShift;
196.                      ArrayResize(TicksLocal, def_MaxSizeArray);
197.                      C_Simulation *pSimulator = new C_Simulation(nDigits);
198.                      if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) > 0)
199.                         nShift += ArrayCopy(m_Ticks.Info, TicksLocal, nShift, 0, c1);
200.                      delete pSimulator;
201.                      ArrayFree(TicksLocal);
202.                      if (c1 < 0) return 0;
203.                   }
204.                   rate.spread = MemShift;
205.                   MemShift = nShift;
206.                   ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
207.                };
208.                m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
209.             }
210.             if (!ToReplay)
211.             {
212.                ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
213.                ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
214.                CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
215.                dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
216.                m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
217.                m_Ticks.nTicks = MemNTicks;
218.                ArrayFree(RatesLocal);
219.             }else   m_Ticks.nTicks = nShift;
220.                            
221.             return dtRet;
222.          };
223. //+------------------------------------------------------------------+

来自文件 C_FileTicks.mqh 的代码片段

此代码片段突出显示了 C_FileTicks.mqh 文件中需要修改的行。请注意,应删除第 149 行,或者更准确地说,将其重新定位到新位置:第 158 行。但为什么要做出这样的改变呢?耐心点,亲爱的读者。一切都将在适当的时候解释。现在,请注意已添加新行:第 152 行。请密切注意这一点:这里的功能是通过模拟将柱形转换为分时报价。在第 152 行,我们捕获新柱开始处的索引值。然后将该值存储在柱形的价差字段中。

让我们继续下一个函数,在那里我们将做一些非常相似的事情。您会注意到,在负责读取分时报价的函数中只添加了一行新行 — 第 204 行。但请记住这个关键点:即使我们从文件中读取分时报价,在某些情况下也可能需要丢弃这些分时报价并用模拟分时替换。我在之前的文章中讨论过这个问题,并解释了这种方法背后的原因。因此,我们真正感兴趣的值是内存索引 MemShift,它告诉我们新柱形从哪里开始。正如我们在前一个函数中所做的那样,我们现在将这个值存储在 MqlRates 结构的 spread 字段中。

那么,我们为什么要这样做呢?实际目的是什么?现在让我们澄清一下这一点。在上一个函数中模拟分时报价到柱形图的过程中,我们准确地知道每个柱形开始的时间和位置。这是因为我们在模拟之前就已经将索引直接指向了柱形的起始位置。这里同样如此,在从文件加载报价的函数中。例如,在第 190 行,分时报价被转换为柱形,就像它们在 C_Replay 类中一样。因此,每次在这里创建新的柱形时,我们都能准确地知道它的起点。这意味着我们不再需要 C_Replay 类来手动确定这个起点。由于 spread 字段在我们的回放/模拟环境中没有实际用途,我们将其重新用于存储一些有价值的东西:每个柱形图的精确起始索引。

如果您回想一下上一节,执行快进的过程包括第 129 行的计算。该计算确定了我们需要跳到的确切指标,以便有效地快速推进模拟。开始看到在分时报价加载期间生成的这个值的重要性了吗?这很关键,因为它使我们能够显著加快快进过程。因此,没有必要逐一重建每个柱。我们可以直接跳到正确的点,然后要求 MetaTrader 5 呈现并更新先前位置和当前位置之间的柱形。换句话说,我们现在有了新的处理事情的方法。

为了有效利用这些数据,我们需要修改 C_Replay 类的几个方面。幸运的是,与我们在上一节中讨论的内容相比,这些变化很小。接下来,您需要通过相应地更新 C_Replay.mqh 文件来包含下面代码片段中显示的更改。

013. #define def_MaxSlider             (def_MaxPosSlider + 1)

              ...

124. //+------------------------------------------------------------------+
125.       void AdjustPositionToReplay(void)
126.          {
127.             int nPos, nCount;
128.             
129.             if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return;
130.             nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider);
131.             for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread);
132.             if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1);
133.             while ((nPos > m_Infos.CountReplay) && def_CheckLoopService)
134.                CreateBarInReplay(false);
135.          }
136. //+------------------------------------------------------------------+

来自文件 C_Replay.mqh 的代码片段

如您所见,我们必须向 C_Replay.mqh 头文件添加新的代码行。这是第 13 行,我们对整体偏移计算应用了小幅修正。再说一遍,这里的一切都有其原因和目的。如果您不做这个小调整,您要么会遇到另一个问题,要么需要以不同的方式处理事情。为了避免不得不重做大部分数据建模,我更喜欢在这里进行简单的调整。那么,第 13 行的调整对我们为什么重要?原因是,如果没有它,您将受到偏移的影响,滑块在控制指示器中的最终位置将提前结束模拟或回放。只需在此处添加一个位置,我们就可以确保图表上仍然可以应用一些额外的分时报价。这实际上是有益的,因为它可以保证回放或模拟过程更顺利地结束。

但真正重要的部分是在第 131 行和第 132 行。只需添加这两行,我们就实现了比以前快得多的快进。尽管如此,可能仍有一些未处理的报价,必须按照之前的方式处理。这些分时报价将使用从第 133 行开始的循环。然而,由于这些剩余的分时报价通常很少,因此该过程仍然相当快。

那么,我们在这里究竟在做什么?在第 131 行,我们搜索其值紧接着位于目标位置下方的柱形的索引。这完全是在 for 循环中完成的。尽管这种结构对于大多数人来说可能看起来不寻常,但它却运行得非常好。它之所以看起来不寻常,是因为我将 CountReplay 值的赋值直接放在了循环声明中。但如果您愿意的话,您当然可以将此任务移到循环之外。

然后在第 132 行,我们必须检查 nCount 的值。这是因为我不想冒险在 MQL5 库中调用 CustomRatesUpdate 失败,因为它可能无法正确解释要处理的数据点或柱形的数量。该函数的其余部分已在上一节中解释过。这些最新变化的有趣之处在于,C_Replay.mqh 文件中的最终代码必须再次更新。由于这些修改很简单,不需要进一步解释,我将简单地向您展示代码的最终版本(至少目前是这样)。您可以在下面找到完整的代码

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. #include "C_ConfigService.mqh"
005. #include "C_Controls.mqh"
006. //+------------------------------------------------------------------+
007. #define def_IndicatorControl   "Indicators\\Market Replay.ex5"
008. #resource "\\" + def_IndicatorControl
009. //+------------------------------------------------------------------+
010. #define def_CheckLoopService ((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != ""))
011. //+------------------------------------------------------------------+
012. #define def_ShortNameIndControl    "Market Replay Control"
013. #define def_MaxSlider              (def_MaxPosSlider + 1)
014. //+------------------------------------------------------------------+
015. class C_Replay : public C_ConfigService
016. {
017.    private   :
018.       struct st00
019.       {
020.          C_Controls::eObjectControl Mode;
021.          uCast_Double               Memory;
022.          ushort                     Position;
023.          int                        Handle;
024.       }m_IndControl;
025.       struct st01
026.       {
027.          long     IdReplay;
028.          int      CountReplay;
029.          double   PointsPerTick;
030.          MqlTick  tick[1];
031.          MqlRates Rate[1];
032.       }m_Infos;
033.       stInfoTicks m_MemoryData;
034. //+------------------------------------------------------------------+
035. inline bool MsgError(string sz0) { Print(sz0); return false; }
036. //+------------------------------------------------------------------+
037. inline void UpdateIndicatorControl(void)
038.          {
039.             double Buff[];
040.                                  
041.             if (m_IndControl.Handle == INVALID_HANDLE) return;
042.             if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
043.             {
044.                if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
045.                   m_IndControl.Memory.dValue = Buff[0];
046.                if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)
047.                   m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
048.             }else
049.             {
050.                m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
051.                m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
052.                m_IndControl.Memory._8b[7] = 'D';
053.                m_IndControl.Memory._8b[6] = 'M';
054.                EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
055.             }
056.          }
057. //+------------------------------------------------------------------+
058.       void SweepAndCloseChart(void)
059.          {
060.             long id;
061.             
062.             if ((id = ChartFirst()) > 0) do
063.             {
064.                if (ChartSymbol(id) == def_SymbolReplay)
065.                   ChartClose(id);
066.             }while ((id = ChartNext(id)) > 0);
067.          }
068. //+------------------------------------------------------------------+
069. inline void CreateBarInReplay(bool bViewTick)
070.          {
071.             bool    bNew;
072.             double dSpread;
073.             int    iRand = rand();
074. 
075.             if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew))
076.             {
077.                m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay];
078.                if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
079.                {                  
080.                   dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
081.                   if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
082.                   {
083.                      m_Infos.tick[0].ask = m_Infos.tick[0].last;
084.                      m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
085.                   }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
086.                   {
087.                      m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
088.                      m_Infos.tick[0].bid = m_Infos.tick[0].last;
089.                   }
090.                }
091.                if (bViewTick) CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
092.                CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
093.             }
094.             m_Infos.CountReplay++;
095.          }
096. //+------------------------------------------------------------------+
097.       void AdjustViewDetails(void)
098.          {
099.             MqlRates rate[1];
100. 
101.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
102.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
103.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE);
104.             m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
105.             CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate);
106.             if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE))
107.                for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++);
108.             if (rate[0].close > 0)
109.             {
110.                if (GetInfoTicks().ModePlot == PRICE_EXCHANGE)
111.                   m_Infos.tick[0].last = rate[0].close;
112.                else
113.                {
114.                   m_Infos.tick[0].bid = rate[0].close;
115.                   m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick);
116.                }               
117.                m_Infos.tick[0].time = rate[0].time;
118.                m_Infos.tick[0].time_msc = rate[0].time * 1000;
119.             }else
120.                m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay];
121.             CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
122.          }
123. //+------------------------------------------------------------------+
124.       void AdjustPositionToReplay(void)
125.          {
126.             int nPos, nCount;
127.             
128.             if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return;
129.             nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider);
130.             for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread);
131.             if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1);
132.             while ((nPos > m_Infos.CountReplay) && def_CheckLoopService)
133.                CreateBarInReplay(false);
134.          }
135. //+------------------------------------------------------------------+
136.    public   :
137. //+------------------------------------------------------------------+
138.       C_Replay()
139.          :C_ConfigService()
140.          {
141.             Print("************** Market Replay Service **************");
142.             srand(GetTickCount());
143.             SymbolSelect(def_SymbolReplay, false);
144.             CustomSymbolDelete(def_SymbolReplay);
145.             CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
146.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
147.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
148.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
149.             CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
150.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
151.             SymbolSelect(def_SymbolReplay, true);
152.             m_Infos.CountReplay = 0;
153.             m_IndControl.Handle = INVALID_HANDLE;
154.             m_IndControl.Mode = C_Controls::ePause;
155.             m_IndControl.Position = 0;
156.             m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState;
157.          }
158. //+------------------------------------------------------------------+
159.       ~C_Replay()
160.          {
161.             SweepAndCloseChart();
162.             IndicatorRelease(m_IndControl.Handle);
163.             SymbolSelect(def_SymbolReplay, false);
164.             CustomSymbolDelete(def_SymbolReplay);
165.             Print("Finished replay service...");
166.          }
167. //+------------------------------------------------------------------+
168.       bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate)
169.          {
170.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
171.                return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket.");
172.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
173.                return MsgError("Asset configuration is not complete, need to declare the ticket value.");
174.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
175.                return MsgError("Asset configuration not complete, need to declare the minimum volume.");
176.             SweepAndCloseChart();
177.             m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1);
178.             if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl"))
179.                Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl");
180.             else
181.                Print("Apply template: ", szNameTemplate, ".tpl");
182. 
183.             return true;
184.          }
185. //+------------------------------------------------------------------+
186.       bool InitBaseControl(const ushort wait = 1000)
187.          {
188.             Print("Waiting for Mouse Indicator...");
189.             Sleep(wait);
190.             while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200);
191.             if (def_CheckLoopService)
192.             {
193.                AdjustViewDetails();
194.                Print("Waiting for Control Indicator...");
195.                if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false;
196.                ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle);
197.                UpdateIndicatorControl();
198.             }
199.             
200.             return def_CheckLoopService;
201.          }
202. //+------------------------------------------------------------------+
203.       bool LoopEventOnTime(void)
204.          {         
205.             int iPos;
206. 
207.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
208.             {
209.                UpdateIndicatorControl();
210.                Sleep(200);
211.             }
212.             m_MemoryData = GetInfoTicks();
213.             AdjustPositionToReplay();
214.             iPos = 0;
215.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
216.             {
217.                if (m_IndControl.Mode == C_Controls::ePause) return true;
218.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
219.                CreateBarInReplay(true);
220.                while ((iPos > 200) && (def_CheckLoopService))
221.                {
222.                   Sleep(195);
223.                   iPos -= 200;
224.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
225.                   UpdateIndicatorControl();
226.                }
227.             }
228. 
229.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
230.          }
231. };
232. //+------------------------------------------------------------------+
233. #undef macroRemoveSec
234. #undef def_SymbolReplay
235. #undef def_CheckLoopService
236. #undef def_MaxSlider
237. //+------------------------------------------------------------------+

C_Replay.mqh文件的最终源代码


更新柱形时间和报价百分比

这个问题解决起来相对简单。我们需要做的就是向鼠标指标发送消息,以便它能够正确解释并显示相关信息。当前的任务是让指标显示距离新柱形开始还剩多少时间,以及前一天收盘价和当前报价之间的百分比变化。

为了使事情更容易,我们将首先处理百分比调整。如果您愿意,欢迎您根据我在这里介绍的方法创建自己的方法。请随时以最适合您需求的方式进行调整。让我们从理解百分比的问题开始。如果您看一下鼠标指标,您会注意到前一个收盘价和当前报价之间的百分比变化没有正确显示。但是,基于鼠标位置的值是准确的。为什么会有这样的差异呢?乍一看,您可能会认为这是因为鼠标指标不了解历史数据在哪里结束以及模拟或回放在哪里开始。但事实并非如此。鼠标指标能够正确读取和解释数据,我们可以通过观察移动鼠标时的变化来检查。实际情况是,某些因素导致鼠标指示器误解数据并偶尔显示奇怪的值。也就是说,它有时确实会显示正确的值。这就是我们需要解决的问题。

解决办法其实很简单。然而,我想提醒一下:你应该避免过度使用我即将展示的方法。如果你不小心,你可能会失去对开发逻辑和流程的控制。话虽如此,让我们看看问题是如何解决的。

第一步,修改鼠标指标的代码,如下所示:
09. #property indicator_chart_window
10. #property indicator_plots 0
11. #property indicator_buffers 1
12. //+------------------------------------------------------------------+
13. double GL_PriceClose;
14. //+------------------------------------------------------------------+
15. #include <Market Replay\Auxiliar\Study\C_Study.mqh>
16. //+------------------------------------------------------------------+
17. C_Study *Study       = NULL;
18. //+------------------------------------------------------------------+
19. input color user02   = clrBlack;                         //Price Line
20. input color user03   = clrPaleGreen;                     //Positive Study
21. input color user04   = clrLightCoral;                    //Negative Study
22. //+------------------------------------------------------------------+
23. C_Study::eStatusMarket m_Status;
24. int m_posBuff = 0;
25. double m_Buff[];
26. //+------------------------------------------------------------------+
27. int OnInit()
28. {
29.    ResetLastError();
30.    Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04);
31.    if (_LastError != ERR_SUCCESS) return INIT_FAILED;
32.    if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay)
33.    {
34.       MarketBookAdd((*Study).GetInfoTerminal().szSymbol);
35.       OnBookEvent((*Study).GetInfoTerminal().szSymbol);
36.       m_Status = C_Study::eCloseMarket;
37.    }else
38.       m_Status = C_Study::eInReplay;
39.    SetIndexBuffer(0, m_Buff, INDICATOR_DATA);
40.    ArrayInitialize(m_Buff, EMPTY_VALUE);
41.    
42.    return INIT_SUCCEEDED;
43. }
44. //+------------------------------------------------------------------+
45. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[],
46.                 const double& high[], const double& low[], const double& close[], const long& tick_volume[], 
47.                 const long& volume[], const int& spread[]) 
48. //int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
49. {
50.    GL_PriceClose = close[rates_total - 1];
51.    m_posBuff = rates_total;
52.    (*Study).Update(m_Status);   
53.    
54.    return rates_total;
55. }
56. //+------------------------------------------------------------------+

鼠标指针代码片段

请注意,在第 13 行,我添加了一个变量。这个变量是全局的。我并不是说“全局”仅仅因为它在函数或过程之外声明,而是因为它在代码中的作用域和位置而真正具有全局性。现在,这个变量并没有做任何特别神奇的事情。然而,在第 50 行,它接收了 MetaTrader 5 提供的值。还要注意,第 48 行(之前包含 OnCalculate 事件函数的声明)已被新版本取代。这非常重要。现在,我们可以利用第 13 行声明的变量来解决我们之前讨论的百分比问题。下一个更改应在 C_Study.mqh 头文件中的代码中进行。如下所示:

41. //+------------------------------------------------------------------+
42.       void Draw(void)
43.          {
44.             double v1;
45.             
46.             if (m_Info.bvT)
47.             {
48.                ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 18);
49.                ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo);
50.             }
51.             if (m_Info.bvD)
52.             {
53.                v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2);
54.                ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1);
55.                ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP));
56.                ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1)));
57.             }
58.             if (m_Info.bvP)
59.             {
60.                v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2);
61.                v1 = NormalizeDouble((((iClose(GetInfoTerminal().szSymbol, PERIOD_D1, 0) - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2);
62.                ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1);
63.                ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP));
64.                ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1)));
65.             }
66.          }
67. //+------------------------------------------------------------------+

来自文件 C_Study.mqh 的代码片段

您会看到,第 61 行(之前包含旧的指示逻辑)已被第 60 行的新逻辑所取代。请注意,这里引用的是指标文件中声明的全局变量。这怎么可能?原因是我们将变量声明为全局变量。或者更确切地说,在文件的全局范围内。这允许从当前正在构建的代码中的任何点访问它。这种全局访问有时可能会导致问题。这就是为什么在处理全局变量时必须始终保持谨慎。

当我需要使用全局变量时(通常是出于非常具体的原因),我会有意识地这样做,因为我知道如果不小心处理,它们可能会造成麻烦。这就是为什么我通常在任何 #include 语句之前定义它们并在它们前面加上 GL_ 以帮助清楚地识别它们。尽管我们在第 25 行也有一个全局范围的变量,但我并不太担心。它有一个非常具体的目的,不太可能被无意中改变。请注意第13行中的变量 — 该变量确实需要格外小心,因为很容易在不知不觉中意外修改它。

通过这个简单的更改,我们解决了百分比偶尔显示不正确值的问题。此外,我们还获得了一些性能改进。这是因为我们不再需要使用 iClose 来获取收盘价。MetaTrader 5 现在直接提供此信息,从而节省了我们手动检索的开销。


结论

虽然我们还没有解决如何追踪直到某个条形图关闭为止的剩余时间的问题,特别是在回放/模拟的背景下,但我们在本文中取得了很大进展。应用程序现在与我们依赖全局终端变量时的行为非常一致。我想你们中的许多人都会惊讶于使用简单的 MQL5 可以完成如此多的工作,而无需依赖任何外部资源。然而,这仅仅是开始。还有很多事情要做,每一步都会带来挑战和新的学习机会。

虽然我还没有解释如何让 MetaTrader 5 在使用重放/模拟模式时通知我们剩余的柱时间,但我将在下一篇文章的开头介绍这一点。不要错过它,因为我们还将开始改进系统的另一个方面,这些方面需要改进,以便在当前设置中无缝工作。

不幸的是,这仍然不是非程序员能够充分使用应用程序的文章。这仅仅是因为成交时间细节仍未解决。但是,如果你是一名程序员,并且一直在遵循我提出的更改,你会看到回放/模拟功能已经如下面的演示视频所示。下篇文章再见。 


演示视频

本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/12265

附加的文件 |
Anexo.zip (420.65 KB)
探索 MQL5 中的密码学:深入浅出的方法阐述 探索 MQL5 中的密码学:深入浅出的方法阐述
本文探讨了在 MQL5 中整合密码学技术,以增强交易算法的安全性和功能性。文章将涵盖关键的密码学方法及其在自动化交易中的实际应用。
使用MQL5经济日历进行交易(第一部分):精通MQL5经济日历的功能 使用MQL5经济日历进行交易(第一部分):精通MQL5经济日历的功能
在本文中,我们首先要了解其核心功能,探讨如何使用MQL5经济日历进行交易。然后,我们在MQL5中实现经济日历的关键功能,以提取与交易决策相关的新闻数据。最后,我们进行总结,展示如何利用这些信息来有效增强交易策略。
ALGLIB 库优化方法(第二部分) ALGLIB 库优化方法(第二部分)
在本文中,我们将继续研究ALGLIB库中剩余的优化方法,并特别关注它们在复杂多维函数上的测试表现。这样我们不仅能够评估每种算法的效率,还能在不同条件下比较出它们的优势与不足。
您应当知道的 MQL5 向导技术(第 40 部分):抛物线止损和反转(PSAR) 您应当知道的 MQL5 向导技术(第 40 部分):抛物线止损和反转(PSAR)
抛物线止损和反转(PSAR) 是趋势确认、和趋势终结点的指标。因为它在识别趋势方面滞后,所以它的主要目的是为持仓定位尾随止损。然而,我们要探索它是否真的可以当作智能系统的交易信号,这要归功于由向导汇编智能系统的自定义信号类。