English Русский Español Deutsch 日本語 Português
preview
开发回放系统(第 76 部分):新 Chart Trade(三)

开发回放系统(第 76 部分):新 Chart Trade(三)

MetaTrader 5示例 |
527 0
Daniel Jose
Daniel Jose

概述

在上一篇文章开发回放系统(第 75 部分):新 Chart Trade(二)中,我解释了 C_ChartFloatingRAD 类的几个方面。然而,由于材料非常密集,我的目的是在相关的情况下提供尽可能详细的解释。还有一个过程有待讨论。即使我把它放在头文件 C_ChartFloatingRAD.mqh 中,并在上一篇文章中试图解释它,这也是不够的。这是因为,为了充分理解 DispatchMessage 过程是如何工作的,需要在它旁边解释另一个主题。

在本文中,我将能够详细介绍 DispatchMessage 过程是如何实际操作的。这是 C_ChartFloatingRAD 类最重要的过程,因为它负责生成和响应 MetaTrader 5 发送给 Chart Trade 的事件。

这里的内容应该结合上一篇文章阅读。我不建议在没有充分掌握之前所涵盖内容的情况下阅读这篇文章。最后两篇文章和这篇文章共同构成了 Chart Trade 指标背后的完整概念基础。因此,了解每一个方面都很重要。

那么,我们继续对 DispatchMessage 进行解释。


理解 DispatchMessage 的工作原理

如果您仍然希望在 Chart Trade 中看到大量被编码的对象,那么您还没有完全理解事物的工作原理。在我们继续之前,我强烈建议重新阅读上一篇文章。这次,我们将研究如何通过响应和生成事件来使 Chart Trade 发挥作用。我们不会在这里创建任何对象。我们的任务只是让 MetaTrader 5 报告的事件正常运行。

在上一篇文章中,您可能已经注意到头文件 C_ChartFloatingRAD.mqh 包含一个缺少代码的区域。缺少的代码显示在下面的代码片段中:

259. //+------------------------------------------------------------------+
260.       void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
261.          {
262. #define macro_AdjustMinX(A, B)    {                          \
263.             B = (A + m_Info.Regions[MSG_TITLE_IDE].w) > x;   \
264.             mx = x - m_Info.Regions[MSG_TITLE_IDE].w;        \
265.             A = (B ? (mx > 0 ? mx : 0) : A);                 \
266.                                  }
267. #define macro_AdjustMinY(A, B)   {                           \
268.             B = (A + m_Info.Regions[MSG_TITLE_IDE].h) > y;   \
269.             my = y - m_Info.Regions[MSG_TITLE_IDE].h;        \
270.             A = (B ? (my > 0 ? my : 0) : A);                 \
271.                                  }
272.                               
273.             static short sx = -1, sy = -1, sz = -1;
274.             static eObjectsIDE obj = MSG_NULL;
275.             short   x, y, mx, my;
276.             double dvalue;
277.             bool b1, b2, b3, b4;
278.             ushort ev = evChartTradeCloseAll;
279.    
280.             switch (id)
281.             {
282.                case CHARTEVENT_CHART_CHANGE:
283.                   x = (short)ChartGetInteger(GetInfoTerminal().ID, CHART_WIDTH_IN_PIXELS);
284.                   y = (short)ChartGetInteger(GetInfoTerminal().ID, CHART_HEIGHT_IN_PIXELS);
285.                   macro_AdjustMinX(m_Info.x, b1);
286.                   macro_AdjustMinY(m_Info.y, b2);
287.                   macro_AdjustMinX(m_Info.minx, b3);
288.                   macro_AdjustMinY(m_Info.miny, b4);
289.                   if (b1 || b2 || b3 || b4) AdjustTemplate();
290.                   break;
291.                case CHARTEVENT_MOUSE_MOVE:
292.                   if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) switch (CheckMousePosition(x = (short)lparam, y = (short)dparam))
293.                   {
294.                      case MSG_TITLE_IDE:
295.                         if (sx < 0)
296.                         {
297.                            DeleteObjectEdit();
298.                            ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false);
299.                            sx = x - (m_Info.IsMaximized ? m_Info.x : m_Info.minx);
300.                            sy = y - (m_Info.IsMaximized ? m_Info.y : m_Info.miny);
301.                         }
302.                         if ((mx = x - sx) > 0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, mx);
303.                         if ((my = y - sy) > 0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, my);
304.                         if (m_Info.IsMaximized)
305.                         {
306.                            m_Info.x = (mx > 0 ? mx : m_Info.x);
307.                            m_Info.y = (my > 0 ? my : m_Info.y);
308.                         }else
309.                         {
310.                            m_Info.minx = (mx > 0 ? mx : m_Info.minx);
311.                            m_Info.miny = (my > 0 ? my : m_Info.miny);
312.                         }
313.                         break;
314.                      case MSG_BUY_MARKET:
315.                         ev = evChartTradeBuy;
316.                      case MSG_SELL_MARKET:
317.                         ev = (ev != evChartTradeBuy ? evChartTradeSell : ev);
318.                      case MSG_CLOSE_POSITION:
319.                         if ((m_Info.IsMaximized) && (sz < 0))
320.                         {
321.                            string szTmp = StringFormat("%d?%s?%c?%d?%.2f?%.2f", ev, _Symbol, (m_Info.IsDayTrade ? 'D' : 'S'), m_Info.Leverage, 
322.                                                 FinanceToPoints(m_Info.FinanceTake, m_Info.Leverage), FinanceToPoints(m_Info.FinanceStop, m_Info.Leverage));
323.                            PrintFormat("Send %s - Args ( %s )", EnumToString((EnumEvents) ev), szTmp);
324.                            sz = x;
325.                            EventChartCustom(GetInfoTerminal().ID, ev, 0, 0, szTmp);
326.                            DeleteObjectEdit();
327.                         }
328.                         break;
329.                   }else
330.                   {
331.                      sz = -1;
332.                      if (sx > 0)
333.                      {
334.                         ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);                  
335.                         sx = sy = -1;
336.                      }
337.                   }
338.                   break;
339.                case CHARTEVENT_OBJECT_ENDEDIT:
340.                   switch (obj)
341.                   {
342.                      case MSG_LEVERAGE_VALUE:
343.                      case MSG_TAKE_VALUE:
344.                      case MSG_STOP_VALUE:
345.                         dvalue = StringToDouble(ObjectGetString(GetInfoTerminal().ID, m_Info.szObj_Editable, OBJPROP_TEXT));
346.                         if (obj == MSG_TAKE_VALUE)
347.                            m_Info.FinanceTake = (dvalue <= 0 ? m_Info.FinanceTake : dvalue);
348.                         else if (obj == MSG_STOP_VALUE)
349.                            m_Info.FinanceStop = (dvalue <= 0 ? m_Info.FinanceStop : dvalue);
350.                         else
351.                            m_Info.Leverage = (dvalue <= 0 ? m_Info.Leverage : (short)MathFloor(dvalue));
352.                         AdjustTemplate();
353.                         obj = MSG_NULL;
354.                         ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable);
355.                         break;
356.                   }
357.                   break;
358.                case CHARTEVENT_OBJECT_CLICK:
359.                   if (sparam == m_Info.szObj_Chart) if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) switch (obj = CheckMousePosition(x = (short)lparam, y = (short)dparam))
360.                   {
361.                      case MSG_DAY_TRADE:
362.                         m_Info.IsDayTrade = (m_Info.IsDayTrade ? false : true);
363.                         DeleteObjectEdit();
364.                         break;
365.                      case MSG_MAX_MIN:
366.                         m_Info.IsMaximized = (m_Info.IsMaximized ? false : true);
367.                         DeleteObjectEdit();
368.                         break;
369.                      case MSG_LEVERAGE_VALUE:
370.                         CreateObjectEditable(obj, m_Info.Leverage);
371.                         break;
372.                      case MSG_TAKE_VALUE:
373.                         CreateObjectEditable(obj, m_Info.FinanceTake);
374.                         break;
375.                      case MSG_STOP_VALUE:
376.                         CreateObjectEditable(obj, m_Info.FinanceStop);
377.                         break;
378.                   }
379.                   if (obj != MSG_NULL) AdjustTemplate();
380.                   break;
381.                case CHARTEVENT_OBJECT_DELETE:
382.                   if (sparam == m_Info.szObj_Chart) macro_CloseIndicator(C_Terminal::ERR_Unknown);
383.                   break;
384.             }
385.             ChartRedraw();
386.          }
387. //+------------------------------------------------------------------+
388. };
389. //+------------------------------------------------------------------+

C_ChartFloatingRAD.mqh 中缺少的代码

缺失的代码完美地填补了这一空白,如下面突出显示的代码片段所示。为了完成 C_ChartFloatingRAD 类代码,应将此片段准确插入到指定行号之后的位置。这应该很容易做到。现在让我们看看它是如何工作的。

首先要注意的是,此片段不包含创建对象的调用。正如我提到的,我们在这里不是编程对象。Chart Trade 被创建并存储为模板。这完全在 MetaTrader 5 中完成。在上一篇文章中,我在末尾留下了一些参考文献,以帮助您理解这个概念。

大多数开发人员并不常用这种编程。但在我看来,在许多情况下,它更实用、更简单。您不必手动编码和定位每个对象,相反,您可以简单地在模板中引用它们。只有当 MetaTrader 5 无法直接配置它们时,我们才会以编程方式创建它们。如前所述,创建元素并将其保存为模板是一项相当罕见的活动。唯一不能通过这种方式处理的对象是 OBJ_EDIT 。这是因为创建文本输入和操作的逻辑比简单地创建 OBJ_EDIT 对象本身更麻烦。因此,这是唯一真正被创建的对象。

查看该代码片段,您将看到它仅处理事件处理。如果您看过上一篇文章中的视频或运行了附加的可执行文件,这可能会让您感到惊讶。毕竟,代码似乎不包含太多对象,但它的工作原理与所示完全相同。您可能已经假设其他对象出现在 DispatchMessage 中。但正如你所见,没有。没有其他对象。一切只是处理来自 MetaTrader 5 的事件。那么,这是如何工作的呢?

让我们一步一步地分解一下。我们在这里处理 MetaTrader 5 发送的五种类型的事件。在单独查看它们之前,请注意第 273 至 278 行声明了一些变量。声明为静态的变量是那些可以位于类全局范围内的变量。那些标记为静态的变量在调用之间保留其值,并且可能是类级变量,但它们仅在此过程中需要。如果你仔细看,你会发现第 278 行声明的变量不是静态的。它在声明时就被初始化。这避免了与不正确使用此变量相关的问题。分配的值与 Defines.mqh 文件中声明的事件相对应。

我们从第 282 至 290 行出现的 CHARTEVENT_CHART_CHANGE 事件开始。只要图表发生变化,它就会触发。它执行简单的计算来定位并正确维护图表窗口内的 OBJ_CHART 对象。

第二个事件, CHARTEVENT_MOUSE_MOVE,显示在 291 至 338 行,是 DispatchMessage 中最长的处理过程。只要鼠标移动或按下鼠标按钮就会触发此事件。但默认情况下,MetaTrader 5 不会发送它。我们必须明确指定要接收鼠标事件。这些事件是通过鼠标指标从 MetaTrader 5 请求的。该指标在之前的文章中有详细描述。了解鼠标指标至关重要,因为它使用户能够与我们正在创建的元素进行交互。

值得注意的是,我们不在这里直接处理鼠标事件。相反,在第 292 行,第一步是检查是否发生左键单击。但我们还需要用鼠标指标来确认其有效性。我们为什么要这样做?为什么我们不直接在处理程序中检查点击是否有效?为什么要问鼠标指标呢?

因为鼠标指标可能正在“研究”模式下运行。当根据用户请求打开此模式时,不应处理任何点击。我们无法可靠地检测鼠标指标何时处于“研究”模式以及处于多长时间。因此,最好的方法是查询鼠标指标。如果点击有效,我们会从 MetaTrader 5 转换位置数据并检查点击了哪个对象。这是通过 CheckMousePosition 完成的。我们还可以使用鼠标指标提供的坐标,但在这种情况下,我们需要读取指标缓冲区。由于这些值与 MetaTrader 5 直接提供的值相同,因此读取缓冲区来获取该信息毫无意义。

在我们分析如何处理实际点击之前,让我们看看第 329 行发生的情况,该行处理 CHARTEVENT_MOUSE_MOVE 事件未收到有效点击或由用户与鼠标交互的任何移动引起的情况。在这种情况下,在第 331 行, sz 变量被赋予了一个负值。然后,在第 332 行,我们检查变量 sx 是否具有正值。如果是的话,我们将继续处理鼠标事件,但现在我们先不讨论这个。因此,在第 334 行,我们通知 MetaTrader 5 鼠标可以移动图表。然后,在第 335 行中,我们为变量 sxsy 分配负值,这导致第 332 行中的检查失败,并防止第 334 行不必要地执行。

需要注意的是,鼠标事件 CHARTEVENT_MOUSE_MOVE 触发极其频繁。在某些情况下, CHARTEVENT_MOUSE_MOVE 发生的频率甚至比 OnTick 更高。因此,如果对这些频繁发生的事件处理不当,可能会严重降低 MetaTrader 5 的速度。这就是它们默认被禁用的原因。

好的,我们已经解决了这个问题。第 293 至 328 行包含与特定 Chart Trade 元素交互的逻辑。当 CheckMousePosition 返回一个值时,它可以指向这部分代码中提到的对象之一,例如标题栏、市场买入、市场卖出和平仓按钮。其他元素在其他地方处理,稍后将进行描述,但这四个元素中的每一个都需要不同的方法。让我们从最难的部分开始:标题栏。

当点击标题栏时,用户可能想要移动 Chart Trade。因此,第 295 行至第 313 行包含所需的逻辑。我们现在不会详细介绍,因为这个过程已经在本系列的另一篇文章中进行了描述,我们在该篇文章中创建了 Chart Trade 的第一个版本。如果您有任何疑问,请阅读前面的文章,它们对我们在这里开发的内容也很重要。记住,代码并不是凭空而来的:它是逐渐创建和实现的,直到它呈现出你现在看到的形式。

我们感兴趣的部分位于第 314 行和第 328 行之间,因此请注意第 278 行定义的值。这很重要,因为在这里我们将测试所使用的三个条件中的两个。当我们处理三种情况时,我们检查的第一种情况是是否点击了 “市场买入” 按钮。如果这是 true,则会为 “ev” 变量分配一个特定的值来指示市场买入事件。这是在第 315 行完成的。

现在请注意:在第 316 行,我们检查是否点击了“市场卖出”按钮。但是,我们还需要检查是否点击了“市场购买”按钮,以便为 “ev” 变量正确分配值。如果我们正在处理另一个事件,为什么需要进行此项检查?确实,这是一个独立的事件。

但重点是,所有三个事件(市场买入、市场卖出和平仓)都执行相同类型的操作。事件之间没有停顿,相反,它们是按顺序排列的。因此,在第 317 行我们需要检查前一个事件是否改变了 ev 变量的值。如果这是 true,那么我们不应该对该行中的下一个事件执行此操作。否则,只触发最后一个事件,忽略前面的事件。

无论如何,我们总会到达第 318 行,此处处理平仓事件。对于此事件,MetaTrader 5 需要进行检查才能运行自定义事件。现在我们将看看如何达到这一点,以避免可能的失败。

第 319 行检查 Chart Trade 是否已最大化。这个检查很重要,因为 CheckMousePosition 函数没有考虑窗口是否最大化。请注意,如果我们不进行此检查并单击其中一个按钮的位置,即使窗口未最大化,自定义事件也会触发。为了避免这个问题,并确保只有当对象在图表上可见时才会触发事件,我们检查了这种情况。

这里有一个重要的问题:窗口必须最大化,也就是说,按钮必须可见。然而,这项检查存在一个小问题。执行此检查的是 Chart Trade 代码,而不是 MetaTrader 5。为什么这很重要?当按钮被另一个对象覆盖时就会出现问题。如果我们放置某些东西覆盖按钮并且 Chart Trade 最大化,那么当我们点击按钮所在的区域时,触发自定义事件的是我们的代码,而不是 MetaTrader 5。我计划在未来修复这个错误。然而,既然这不是那么关键,我们现在可以忍受。但如果您在真实账户上使用 Chart Trade,则应该考虑到这个问题。当 Chart Trade 最大化时,请勿在按钮前面放置任何东西

同一行 319 中还有另一个检查,它可防止通过单击鼠标后移动鼠标触发多个事件。这是一个简单的检查,其中 sz 的值必须为负数。如果为正,则表示事件已经触发并且鼠标按钮仍处于按下状态。当释放按钮并且鼠标移动时,第 331 行将确保触发新的用户事件。虽然这是一个简单的检查,但它可以避免很多问题。

所有的“魔法”都发生在第 321 行。为了避免它太长,我把它分成了两部分。因此,将第 322 行视为第 321 行的一部分。在解释这一行之前,我们先看看接下来的四行做了什么。第 323 行将事件记录到 MetaTrader 5 工具箱。你可能认为这没有必要,但相信我,这将帮助你避免许多问题。这是一种非常宝贵的调试实践。因此,永远不要忽视 MetaTrader 5 工具箱中出现的消息,其中许多消息非常重要。

第 324 行只是执行锁定以防止前面描述的情况,因为第 319 行的检查中使用了变量 sz 。自定义事件实际上是在第 325 行触发的。请注意,与控制指标中所做的不同,这里的数据是在字符串字段中发送的。如果这不可能,我们将在应用程序之间传输大量信息时遇到问题。

现在,这里有一个重要的细节。MQL5 中的字符串遵循与 C/C++ 中相同的原则 —— 它们以 NULL 字符结尾。我为什么要提到这一点?因为它解释了为什么我们需要在这里使用 StringFormat 函数。如果您认为没有必要打印通过自定义事件发送的内容,您可以简单地从第 321 行获取内容并将其放在第 325 行,用 StringFormat 函数替换变量 szTmp ,并使用与第 321 行完全相同的表达式。

回到字符串的问题:问题是我们需要在字符串中传递数值。此时,您可能会想:为什么不使用消息的 lparam 和 dparam 字段?为什么我们必须使用 sparam?原因很简单,我们需要传递的数据量很大。如果我们只传递几个参数,我们可以使用 lparam 和 dparam。但由于信息量很大,我们必须使用 sparam 字段。这就是许多人,尤其是初学者或有抱负的程序员犯错误的地方。

当您查看一个数字(例如,值 65)时,一个应用程序可能将其解释为数字 65,而另一个应用程序可能将其解释为字母 “A”。许多读者可能会觉得这很令人困惑,但问题在于你看到的是字面意义上的 65,而不是二进制形式。用二进制表示的话,就是 8 位 0100 0001 。这是处理编程时的正确思维方式。所以,当你看一个字符串时,你不应该把它看作可读的字符或人类可理解的文本。将字符串视为一个非常大的数字 —— 不仅仅是 1 个字节、256 个字节或任何其他固定大小的数字,而且可能是一个巨大的字节序列。

以这种方式思考会更容易理解其他事情:如何标记字符串的末尾?一些编程语言在字符串的开头存储一个值 —— 一个“死”值,表示字符串包含的字符数或字节数。BASIC 和旧的 Pascal 语言就是这种方法的例子。在 Pascal 中,长度存储在字符串的开头。

在这种情况下,字符串(或者更准确地说,数组)可以包含 0 到 255 之间的任何字节值。虽然这种方法在许多情况下效果很好,但在其他情况下效果不佳,因为固定大小的数组会浪费内存。例如,你可能只需要 3 个字节,但如果编译器检测到有时你在同一个数组中需要 50 个字节,它将分配最大值 —— 50 个字节,即使你只需要 3 个。

由于这种效率低下,其他语言不使用这种字符串格式。相反,它们定义了一个特定的代码或值来标记字符串的结束。最常用的方法是使用 NULL 字符。用二进制表示就是 0000 0000。这个特定字符的选择来自于编译器的实现决策。理论上可以使用任何字符。这里重要的是,当我们需要在字符串中发送消息时,这会如何影响我们的消息。

让我们回到数值,我们的 Chart Trade 必须传输主要两种类型:short (短整型)和 double(双精度型)。短整型值使用 16 位或 2 个字节。我甚至不会深入讨论“双精度”类型;我将保留“短整型”的示例。在 Chart Trade 中,使用短整型数作为杠杆水平。最小杠杆值为 1。用二进制表示就是:0000 0000 0000 0001 (此处以 16 位形式写入)。

但是,尽管 short 使用 2 个字节,但字符串实际上是由 1 个字节的字符组成的。这意味着字符串中值 1 的第一个字节代表 NULL 字符。这是正确的。在读取任何信息之前,字符串就已经终止了。因此,我们必须将值转换为可打印的字符。换句话说,无论数据的二进制形式如何,我们都会将其转换为人类可读(可打印)的形式进行传输,然后再将其转换回其二进制等效形式。这正是我们需要在这里使用 StringFormat 函数的原因。


最后的探讨

尽管这篇文章似乎突然结束,但我不太愿意在这么短的篇幅内解释第二个必要主题 —— 应用程序之间的通信协议。这是一个复杂的问题,值得详细讨论。我可以简单地说第 321 行创建了我们将使用的协议。

然而,使用这种方法,程序之间的整个通信问题仍然完全模糊,如果你试图理解如何在程序中使用这些知识,这对你没有帮助。尽管 MetaTrader 5 已经存在了相当长的一段时间,许多程序员对它感兴趣,但我还没有看到有人使用我在这里展示的内容。大多数想要实现目标的人都是从头开始,或者创建庞大的程序,在大多数情况下,这些程序极难维护、调整或改进。然而,有了我们在这里分享的知识,您将能够开发更小、更简单的程序,这些程序也更容易调试和改进。

这就是为什么我想借 Chart Trade 的这个机会详细解释消息传递过程的工作原理。我希望作为一名初学者程序员,你会意识到我们可以做的比通常预期的要多得多。

至于其余事件( CHARTEVENT_OBJECT_ENDEDITCHARTEVENT_OBJECT_CLICKCHARTEVENT_OBJECT_DELETE ),我相信您能够自己理解它们,因为它们的代码比我们在此讨论的要简单得多。

在下一篇文章中,我将详细解释为什么第 321 行有其特定的格式,以及它如何影响接收 Chart Trade 事件的 EA 交易代码,使其能够根据用户与其按钮的交互来执行订单。

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

附加的文件 |
Anexo.zip (420.65 KB)
循环孤雌生殖算法(CPA) 循环孤雌生殖算法(CPA)
本文提出了一种新的群体优化算法——循环孤雌生殖算法(CPA),其灵感源自蚜虫独特的生殖策略。该算法融合了两种生殖机制:孤雌生殖(无性繁殖)与有性生殖,并借助蚜虫的群体结构以及群体间的迁徙能力。算法的核心特点包括:在不同生殖策略之间自适应切换和通过“迁飞”机制实现群体间的信息交换。
构建MQL5自优化智能交易系统(EA)(第四部分):动态头寸规模调整 构建MQL5自优化智能交易系统(EA)(第四部分):动态头寸规模调整
成功运用算法交易需要持续的跨学科学习。然而,无限的可能性可能会耗费数年努力,却无法取得切实成果。为解决这一问题,我们提出一个循序渐进增加复杂性的框架,让交易者能够迭代优化策略,而非将无限时间投入不确定的结果中。
黑洞算法(BHA) 黑洞算法(BHA)
黑洞算法(BHA)利用黑洞引力原理来优化解。在本文中,我们将考察 BHA 如何在避免局部极端情况的同时,吸引最佳解,以及为什么该算法已成为解决复杂问题的强大工具。学习简单的思路如何在优化世界带来令人印象深刻的结果。
在训练中激活神经元的函数:快速收敛的关键? 在训练中激活神经元的函数:快速收敛的关键?
本文研究了在神经网络训练背景下,不同激活函数与优化算法之间的相互作用。我们特别关注了经典的 ADAM 算法及其种群版本在处理多种激活函数(包括振荡的 ACON 和 Snake 函数)时的表现。通过使用一个极简的 MLP (1-1-1) 架构和单个训练样本,我们将激活函数对优化的影响与其他因素隔离开来。文章提出了一种通过激活函数边界来管理网络权重的方法,以及一种权重反射机制,这有助于避免训练中的饱和和停滞问题。