市场模拟(第一部分):跨期订单(一)
概述
在上一篇文章“开发回放系统(第 78 部分):新 Chart Trade(五) ,我展示了 EA 交易如何解释 Chart Trade 发送的指令。Chart Trade 实际传输给 EA 交易的信息取决于用户与它的交互。换句话说,当用户点击买入、卖出或平仓按钮时,就会向图表发送一条消息。当附加到此图表时,EA 交易的任务之一是拦截、解码和执行该消息中包含的指令。
虽然这种机制简单可靠,但我们面临一个小问题。嗯,这并不是一个问题,更多的是一种不便。在我们真正开始向交易服务器发送订单之前,需要解决这个不便之处。
如果你不熟悉我的意思,可能是因为你不交易某些资产,更具体地说,期货合约。这些类型的资产都有到期日。通常,两种类型的合约会同时交易:一种是交易量更大的完整合约,另一种是可视为完整合约一小部分的迷你合约。迷你合约允许采用需要较小交易量或较少合约的策略。
我不会在这里详细讨论这些策略。关键在于,有时候,为了制定策略,你需要更少的合约。对此感兴趣的人应该研究一下对冲策略。对于我们程序员来说,真正关心的是当图表显示完整合约时如何执行迷你合约中的交易。
但还有另一个挑战:长期策略。每次合约到期(在固定的、众所周知的日期),就会开始新的系列。对于从事长期限工作、跨越多个系列的交易者来说,这是一个重大问题。这是因为指标和移动平均线必须在每个新系列中重新开始计算。
为了更好地理解这一点,让我们来看看B3(巴西证券交易所)美元期货合约,该合约每月到期。这意味着每个月都会结束一个系列,然后开始一个新的系列。考虑到每个月大约有 20 个交易日(4 周内每周有 5 个交易日),在尝试使用 20 个周期的移动平均线时会出现问题。当移动平均线完全计算和绘制时,合约到期并重置为新的序列。而这只是 20 周期的平均值。其他指标需要更长的时间,受到的影响更大。总之,这是一个大问题。
为了克服这一点,我们可以使用期货合约的历史数据。然而,使用历史数据并不能解决所有问题。它确实给我们程序员带来了新的挑战。请记住,交易者并不关心服务器如何接收或绘制数据,交易者只希望获得准确的信息和可靠的交易执行。作为程序员,我们有责任解决绘图问题,并确保图表中的交易请求正确路由到交易服务器。
在文章从零开始开发 EA 交易(第 11 部分):跨期订单系统,我对此做了一些细节解释。但在这里,由于我们还要处理回放/模拟器,问题就变得更加大了。不过,我们可以运用拿破仑的策略:分而治之。通过将问题分解为更小的部分,我们可以逐步开发一个模拟订单系统。我们将从一个关键方面开始:处理期货合约中的指标。由于美元期货合约是我所知道的最极端的情况,我们将重点关注它。然而,请记住,同样的原则也适用于具有类似复杂性的其他合约。
开始实现
在前面提到的文章中,我们开发了跨期订单系统,但要使其覆盖其他类型的合约相对复杂。在这里,出于实际原因,我们将采取不同的方法来简化此类调整。不是为了交易者,而是为了我们程序员。交易者需要适应我们的实施方式。但作为交换,他们将获得一个简单的选择:选择交易完整合约还是迷你合约。
为了实现这一点(至少在最初,当我们仍在与实时交易服务器通信时),我们需要对现有代码进行一些有针对性的修改。让我们从一个关键事实开始:正如介绍中所述,最好的图表是基于历史数据的图表。但这个历史图表不能直接交易。
为了解决这个问题,我们需要一个系统,将历史图表上的订单路由到交易者实际想要使用的合约。请记住,交易者可能想要交易完整合约或迷你合约。但我们暂时还不必担心这一点。首先,我们必须了解一个简单的事实:图表上显示的内容来自合约的历史数据,句号。
在B3上,期货合约有六种不同的命名约定,这适用于每一份具体合约。这意味着完整合约有六种类型,迷你合约有六种类型。这似乎是一个巨大的复杂问题,但仔细观察后,就会发现情况并没有看上去那么糟糕。尽管有六种变体,但它们实际上可以归结为三种主要类型,每种类型都有两个子变体。
这种简化对我们有很大帮助。尽管如此,我还是建议研究这三种类型之间的差异。它们之间的图表数据差异很大,许多交易者完全没有意识到这一点。如果您仅编写解决方案,请务必通知交易者。如果您既从事编程又从事交易,请更加认真地对待这个建议 —— 如果您不了解这些区别,您可能会遇到严重的问题。
因此,有三种命名类型,每种类型都有两种变体。好,但对于我们程序员来说,重要的不是名称本身,因为这与交易者更相关。对我们来说,关键问题是:这个命名约定中有规则吗?如果可以,我们如何利用它来构建跨期订单系统?
幸运的是,这样的规则确实存在。事实上,我们已经使用它一段时间了。在下面的代码片段中看看这是如何工作的。
38. //+------------------------------------------------------------------+ 39. void CurrentSymbol(void) 40. { 41. MqlDateTime mdt1; 42. string sz0, sz1; 43. datetime dt = macroGetDate(TimeCurrent(mdt1)); 44. enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER; 45. 46. sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3); 47. for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS); 48. switch (eTS) 49. { 50. case DOL : 51. case WDO : sz1 = "FGHJKMNQUVXZ"; break; 52. case IND : 53. case WIN : sz1 = "GJMQVZ"; break; 54. default : return; 55. } 56. for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0)) 57. if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break; 58. } 59. //+------------------------------------------------------------------+
C_Terminal.mqh 文件中的代码
此代码片段来自头文件 C_Terminal.mqh。请注意,在第 44 行我们定义了将支持的期货合约的名称。如果您想使用玉米、牛、标准普尔、欧元等资产,可以将其他合约添加到此列表中。只需记住遵循每个合约的命名规则,以正确识别活动合约。这里描述的程序不会返回过去的合约,也不会返回提前两个或多个到期的合约。它始终解析为当前有效的合约。
为了实现这一点,第 46 行提取资产名称的前三个字符。无论资产是什么,它总是会取得这前三个字符。这是因为 B3(巴西证券交易所)使用前三个字符来标识资产的命名约定。提取后,资产名称将保存在变量中,以便在其余代码中使用。请注意这一事实。
接下来,第 47 行迭代我们定义的合约名称的枚举。这里的目的是找到正确的匹配。这就是为什么在第 44 行的枚举中,名称必须与合约名称本身相似。由于 B3 使用大写字符,枚举也必须大写。一旦找到匹配,或者一旦列表用尽,第 47 行中的循环就结束了。
在第 48 行,我们可以测试检索到的值。如果没有找到匹配项,则执行跳到第 54 行的代码。否则,我们继续构建完整的合约名称。最后的命名发生在第 57 行,在该行中,程序确认正在生成的合约名称对应于当前活动的合约。简而言之,该程序扫描可能的期货合约,直到找到有效的合约。
然而,这里有一个重要的限制。该过程使用从资产名称派生的基本合约名称。这意味着您只能将给定合约的历史数据映射到同一合约的活动版本。使用当前代码,您无法将历史数据从完整合约映射到活动的迷你合约。这就是我们将在本文中解决的不便之处。
通过这样做,我们将使交易者能够选择是交易完整合约还是迷你合约,即使使用其中任何一个的历史数据。我们的目标是通过最少的代码更改来实现这一点,因为我们更改的代码越多,引入错误的可能性就越大。
为了实现这一点,上面的代码片段被修改如下:
38. //+------------------------------------------------------------------+ 39. void CurrentSymbol(bool bUsingFull) 40. { 41. MqlDateTime mdt1; 42. string sz0, sz1; 43. datetime dt = macroGetDate(TimeCurrent(mdt1)); 44. enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER; 45. 46. sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3); 47. for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS); 48. switch (eTS) 49. { 50. case DOL : 51. case WDO : sz1 = "FGHJKMNQUVXZ"; break; 52. case IND : 53. case WIN : sz1 = "GJMQVZ"; break; 54. default : return; 55. } 56. sz0 = EnumToString((eTypeSymbol)(((eTS & 1) == 1) ? (bUsingFull ? eTS : eTS - 1) : (bUsingFull ? eTS + 1: eTS))); 57. for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0)) 58. if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break; 59. } 60. //+------------------------------------------------------------------+
C_Terminal.mqh 文件中的代码
正如您所见,变化很小。第一个是在第 39 行向函数添加一个参数。该参数告诉程序要生成的合约名称应该是完整合约还是迷你合约。选择权在于交易者。作为程序员,我们的责任是让他们能够灵活地使用他们喜欢的任何图表,只要图表中的资产数据在某种程度上与正在交易的合约相对应。当然,我们可以实现更复杂的行为,但不要让需要做的事情过于复杂。
除了第 39 行的这一更改之外,我们还添加了一行新代码。从技术上讲,这种添加并不是严格必要的,因为我们本可以修改现有的代码。但它简化了解释和你对正在发生的事情的理解。这个新行,第 56 行可以直接放置在第 58 行使用变量 sz0 的位置,结果将是相同的。然而,这将使解释变得混乱,更难理解。
那么,第 56 行对变量 sz0 做了什么?让我们来分析一下。本质上,我们忽略了资产名称本身。相反,我们将第 44 行定义的枚举转换为字符串。MQL5 通过 EnumToString 函数允许这样做。
现在,如果你使用的期货合约没有完整版和迷你版,那么一个细节可能会使事情变得复杂。这在大宗商品中相当常见。但在我想要展示的案例中 —— 指数和货币,特别是美元期货 —— 两种合约类型都存在。
除非另有定义,否则枚举始终以零值开始。在我们的例子中,迷你合约被赋予偶数值,而完整合约被赋予奇数值。理解这一点很重要。这些值是二进制的,您应该知道如何选择这个或那个位。在二进制中,最低有效位(最右边的一位)决定数字是偶数还是奇数。通过应用按位与来隔离该位,我们可以检查枚举值是偶数还是奇数。再说一遍:迷你合约是偶数,完整合约是奇数。
因此,如果值是奇数,则执行三元运算符的第一部分。如果是偶数,则执行第二部分。目前,我认为一切都很清楚。然后,在第一个三元运算符的每个分支内,我们使用第二个三元运算符。第二个三元运算符允许我们调整变量 eTS ,确保它反映正确的合约名称。
例如:如果合约是 WDO,那么 eTS 等于 2,即偶数。这将触发第一个三元运算符的第二部分,在其中,第二个三元运算符执行第二次检查。它检查程序调用请求的是完整合约还是迷你合约。
如果交易者要求完整的合约, eTS 就会增加 1。因此,它的值从 2 变为 3。在枚举中,位置 3 对应于 DOL。当 MQL5 执行 EnumToString 时,值 3 被转换为字符串 DOL,从而产生完整的合约名称,即使图表基于迷你美元历史数据。
反过来也是一样的,如果图表显示完整美元合约的历史数据,但交易者请求迷你合约,则第一个三元运算符执行其第一个分支。在其中,第二个三元运算符将 eTS 减 1,将其值从 3 更改为 2。这将其映射回 WDO。
简而言之:在第 47 行找到的值在第 56 行进行了调整,以便合约名称与交易者的选择(迷你或完整)相匹配,同时仍然依赖于其中一个的历史图表。
到目前为止,一切都很好。但是,如果合约没有迷你版本,只有完整合约怎么办?我们该如何处理呢?您可能认为有两种可能的解决方案。事实上,只有一个。如果你试图复制枚举中的值来人为地创建偶数和奇数条目,编译器将拒绝它。相反,解决方案是按逻辑顺序构造枚举。然后,当测试特定值时,如果不存在迷你合约,则变量 sz0 保持不变。在实践中,这需要在代码中进行额外的测试。但那里没有什么复杂的东西。
这样,我们就解决了问题的第一部分。但是,我们还没有完成。要继续,我们需要调整头文件 C_Terminal.mqh 的另一部分:类构造函数。构造函数负责调用我们刚刚修改的过程。因此,现在必须用下面显示的新版本替换原始版本。
72. //+------------------------------------------------------------------+ 73. C_Terminal(const long id = 0, const uchar sub = 0, const bool bUsingFull = false) 74. { 75. m_Infos.ID = (id == 0 ? ChartID() : id); 76. m_Mem.AccountLock = false; 77. m_Infos.SubWin = (int) sub; 78. CurrentSymbol(bUsingFull); 79. m_Mem.Show_Descr = ChartGetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR); 80. m_Mem.Show_Date = ChartGetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE); 81. ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false); 82. ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, true); 83. ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_CREATE, true); 84. ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false); 85. m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); 86. m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); 87. m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); 88. m_Infos.PointPerTick = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE); 89. m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE); 90. m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP); 91. m_Infos.AdjustToTrade = m_Infos.ValuePerPoint / m_Infos.PointPerTick; 92. m_Infos.ChartMode = (ENUM_SYMBOL_CHART_MODE) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_CHART_MODE); 93. if(m_Infos.szSymbol != def_SymbolReplay) SetTypeAccount((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)); 94. ChartChange(); 95. } 96. //+------------------------------------------------------------------+
C_Terminal.mqh 文件中的代码
请注意,只进行了两个非常简单的更改。第一个是第 73 行,我们在这里添加了一个新参数。然后在第 78 行使用此参数,我们在此调用前面解释的过程。默认情况下,我将其设置为优先考虑迷你合约。但交易者可以自由选择最适合其策略的选项。为了支持这种灵活性,需要对代码的特定部分进行一些小调整。
由于我们尚未修改 EA 交易代码,因此必须在 Chart Trade 代码中进行必要的更改。为了清楚起见,让我们在单独的一节中介绍一下。
将 Chart Trade 转变为跨期订单系统
Chart Trade 作为跨期订单系统运行所需的改变非常简单。您可以考虑在界面中添加一个对象,以便交易者可以直接切换跨期类型。然而,我在这里不会采取这种做法。我的目标是尽量减少变化。添加这样的对象需要更多的代码来支持此功能。相反,我们可以允许交易者通过指标设置更改跨期类型。这种方法很简单,对现有代码的修改很少。第一步可以在下面的代码中看到。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "Chart Trade Base Indicator." 04. #property description "See the articles for more details." 05. #property version "1.80" 06. #property icon "/Images/Market Replay/Icons/Indicators.ico" 07. #property link "https://www.mql5.com/pt/articles/12536" 08. #property indicator_chart_window 09. #property indicator_plots 0 10. //+------------------------------------------------------------------+ 11. #include <Market Replay\Chart Trader\C_ChartFloatingRAD.mqh> 12. //+------------------------------------------------------------------+ 13. #define def_ShortName "Indicator Chart Trade" 14. //+------------------------------------------------------------------+ 15. C_ChartFloatingRAD *chart = NULL; 16. //+------------------------------------------------------------------+ 17. enum eTypeContract {MINI, FULL}; 18. //+------------------------------------------------------------------+ 19. input ushort user01 = 1; //Leverage 20. input double user02 = 100.1; //Finance Take 21. input double user03 = 75.4; //Finance Stop 22. input eTypeContract user04 = MINI; //Cross order in contract 23. //+------------------------------------------------------------------+ 24. int OnInit() 25. { 26. chart = new C_ChartFloatingRAD(def_ShortName, new C_Mouse(0, "Indicator Mouse Study"), user01, user02, user03, (user04 == FULL)); 27. 28. if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED; 29. 30. return INIT_SUCCEEDED; 31. } 32. //+------------------------------------------------------------------+ 33. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 34. { 35. return rates_total; 36. } 37. //+------------------------------------------------------------------+ 38. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 39. { 40. if (_LastError < ERR_USER_ERROR_FIRST) 41. (*chart).DispatchMessage(id, lparam, dparam, sparam); 42. } 43. //+------------------------------------------------------------------+ 44. void OnDeinit(const int reason) 45. { 46. switch (reason) 47. { 48. case REASON_INITFAILED: 49. ChartIndicatorDelete(ChartID(), 0, def_ShortName); 50. break; 51. case REASON_CHARTCHANGE: 52. (*chart).SaveState(); 53. break; 54. } 55. 56. delete chart; 57. } 58. //+------------------------------------------------------------------+
Chart Trade 指标源代码
在第 17 行,我们添加一个枚举。它帮助交易者(或用户)定义合约类型。请注意,第 22 行使用了此枚举。此时,交易者需要决定 EA 交易是否应该使用完整合约或迷你合约进行操作。这里有一个缺点:理想情况下,应该在 EA 交易中进行选择,而不是在 Chart Trade 中进行选择。但由于 Chart Trade 和 EA 交易目前仍然是独立的实体,我们将保持这种状态。
真正的挑战不在于 Chart Trade,甚至不在于 EA 交易。正如我在上一篇文章中所解释的,Chart Trade 已经可以控制 EA 交易。问题在于稍后将开发的系统的另一部分。真正的困难就在这里,因为最终一切都必须通过 EA 交易。理想情况下,我们需要提供其中所需的一切。不过,目前,为了演示目的,我们将在 Chart Trade 中处理选择。
然后在第 26 行使用此配置值。请注意,我们传递给构造函数的是一个布尔值,而不是一个数值。为什么呢?因为对于最终用户来说,布尔值可能看起来不那么具有描述性。对于我们程序员来说这非常清楚。毕竟,只有两种可能的情况:交易者要么使用完整合约,要么使用迷你合约。因此,从编码的角度来看,布尔值是最合适的选择。然后将布尔值传递给类构造函数。让我们在下面的代码片段中看看它是什么样子的。
213. //+------------------------------------------------------------------+ 214. C_ChartFloatingRAD(string szShortName, C_Mouse *MousePtr, const short Leverage, const double FinanceTake, const double FinanceStop, const bool bUsingFull) 215. :C_Terminal(0, 0, bUsingFull) 216. { 217. m_Mouse = MousePtr; 218. m_Info.IsSaveState = false; 219. if (!IndicatorCheckPass(szShortName)) return; 220. if (!RestoreState()) 221. { 222. m_Info.Leverage = Leverage; 223. m_Info.IsDayTrade = true; 224. m_Info.FinanceTake = FinanceTake; 225. m_Info.FinanceStop = FinanceStop; 226. m_Info.IsMaximized = true; 227. m_Info.minx = m_Info.x = 115; 228. m_Info.miny = m_Info.y = 64; 229. } 230. CreateWindowRAD(170, 210); 231. AdjustTemplate(true); 232. } 233. //+------------------------------------------------------------------+
C_ChartFloatingRAD.mqh 文件片段
此处的变化与 C_Terminal 类的构造函数一样简单。我们仅添加一个由构造函数接收的新参数(第 214 行),然后将其传递给 C_Terminal 构造函数(第 215 行)。就这么简单。一切都简单明了,不言自明。
尽管如此,我们还需要一个小的修改。这次,它是对 C_ChartFloatingRAD 类的补充。这一变化使 Chart Trade 能够向 EA 交易传达交易者实际想要操作的内容。修改内容如下面代码片段所示。
330. case MSG_BUY_MARKET: 331. ev = evChartTradeBuy; 332. case MSG_SELL_MARKET: 333. ev = (ev != evChartTradeBuy ? evChartTradeSell : ev); 334. case MSG_CLOSE_POSITION: 335. if ((m_Info.IsMaximized) && (sz < 0)) 336. { 337. string szTmp = StringFormat("%d?%s?%s?%c?%d?%.2f?%.2f", ev, _Symbol, GetInfoTerminal().szSymbol, (m_Info.IsDayTrade ? 'D' : 'S'), 338. m_Info.Leverage, FinanceToPoints(m_Info.FinanceTake, m_Info.Leverage), FinanceToPoints(m_Info.FinanceStop, m_Info.Leverage)); 339. PrintFormat("Send %s - Args ( %s )", EnumToString((EnumEvents) ev), szTmp); 340. EventChartCustom(GetInfoTerminal().ID, ev, 0, 0, szTmp); 341. } 342. break;
C_ChartFloatingRAD.mqh 文件片段
这种调整非常微小,可能会被忽视。它出现在第 337 行,我们在此添加了一个要发送给 EA 交易的新值。该值告诉 EA 交易哪种资产 - 或者更准确地说,哪种合约 - 正在 Chart Trade 中显示。请记住,此更改将强制 EA 交易进行另一次更新。但我们稍后会处理这个问题。
最后的探讨
我们在本文中所做的工作展示了 MQL5 的灵活性。但它也带来了我们以后需要解决的新挑战。这些事情远非简单,并不总是能完全轻松地解决。实施跨期订单系统,允许 Chart Trade 用户告诉 EA 交易,图表资产不一定是正在交易的资产,这带来了很大的复杂性。我想明确一点:大多数这些问题并不是由 Chart Trade 或 EA 交易本身引起的。
当我们引入我尚未引入的元素,即仍需开发的代码部分时,真正的问题就出现了。允许用户在 Chart Trade 中选择合约类型并不是最好的长期解决方案。至少目前在我看来不是。
未来的变化可能会使在 Chart Trade 中直接配置这一点变得实用且可持续。不过,就目前而言,我更喜欢把事情简单到足以解释清楚。快速构建个人解决方案很容易;构建一个其他人可以理解和应用的模型要求更高。这就是为什么您可以期待此 Chart Trade EA 交易系统的未来更新。它们一定会来的。
在下面的视频中,您可以直接在图表上看到该过程。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/12536
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
使用MQL5经济日历进行交易(第六部分):利用新闻事件分析和倒计时器实现交易入场自动化
MQL5交易策略自动化(第八部分):构建基于蝴蝶谐波形态的智能交易系统(EA)
价格行为分析工具包开发(第十五部分):引入四分位理论(1)——四分位绘图脚本
在 MQL5 中构建自优化智能交易系统(第六部分):防止爆仓