
开发回放系统 — 市场模拟(第 19 部分):必要的调整
概述
我认为从本系列的前几篇文章中可以清楚地看出,我们需要实现一些额外的要点。更好地组织工作是绝对必要的,尤其是一些深入的改进。如果您计划仅用回放/模拟系统来操控一种资产,那么您就不需要我们将要实现的许多东西。您可以把它们放在一边 — 我的意思是它们不必出现在配置文件之中。
不过,您很可能不只用到一种资产,而是若干种不同资产,甚至是相当大的数据库。在这种情况下,我们就需要把事情组织好,由此需要实现额外的代码来达成该目标,尽管在某些非常特殊的情况下,我们能简单地使用源代码中已有的内容,不过是以隐形方式。这只需要将其放到明处。
我总是喜欢把事情安排得井井有条,我猜很多人也会这样想,并尝试这样做。知晓并理解如何实现此功能会很好。此外,如果您需要研究或分析特定资产的特定参数,您肯定要学习如何向系统添加新参数。
在此,我们要做好准备,如此当我们需要往代码里添加新函数时,就能顺滑轻松地发生。当前代码还不能涵盖或处理那些显著推进过程所必需的事情。我们需要将所有东西都结构化,以便能够以最小的工作量实现某些事情。如果我们正确地做好所有事情,我们就能得到一个真正通用的系统,可以轻松地适应任何需要处理的状况。这些方面之一将是下一篇文章的主题。幸运的是,感谢最后两篇文章展示了如何向市场观察窗口添加跳价,事情总体上正按计划前进。如果您错过了这些文章,可以按以下链接去访问它们:开发回放系统 — 市场模拟(第 17 部分):调价和更多跳价(I) ,和开发回放系统 — 市场模拟(第 18 部分):跳价和更多跳价(II)。这两篇文章提供的宝贵信息,都与我们后续文章中将要做的事情有关。
不过,仍然缺少一些非常具体的细节,这些都将在本文中实现。此外,还有其它十分复杂的问题,需要单独的文章来解释如何梳理并解决这些问题。我们来看看在本文中现在开始实现系统。我们将从改进所用的数据组织结构开始。
实现目录系统
此处的问题不在于我们是否真的需要实现这个系统,而是我们为什么要实现它。在当前开发阶段,我们可以使用目录系统。不过,我们将不得不做更多的工作来实现回放/模拟服务。我的意思是,不仅简单地将新变量添加到配置文件当中,还有更多工作。为了明白我在说什么,请看下面的图片:
图例 01 – 访问当前系统中目录的途径。
图例 02 – 访问目录的替代途径。
尽管从回放/模拟系统的角度来看,图例 01 的行为与图例 02 相同,但您很快就会注意到,使用图例 02 所示来配置要实用得多。这是因为我们只需要一次性指定数据所在的目录,回放/建模系统会关照剩下的一切。而使用图例 02 所示的系统时,如果我们使用一个非常大的数据库,就可避免忘记或错误地指定从哪里查找数据,例如添加移动平均线。如果出现这样的编写错误,可能会发生两种状况:
- 在第一种情况下,系统只会发出无法访问数据的警告。
- 在第二种情况下,情况更为严重,将会使用不正确的数据。
不过,由于能够把目录设置在一处,这些类型的错误就能少得多。这并不意味它们根本不会发生,只是更少见。请记住,我们可以将对象组织到更具体的目录当中,从而将图例 01 和图例 02 结合到一起。不过,在此,我将把所有内容都保留在一个更简单的水平上。您的实现方式可随意符合您的数据处理和组织风格。
我们已经见识过理论,现在是时候看看如何在实践中做到这一点了。这个过程相对简单明了,至少与我们尚未做的事情相比是这样。首先,我们为该类创建一个新的私密变量,如下代码所示:
private : enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE}; string m_szPath;
当我们将该变量添加到此位置时,它对于该类内部的所有过程均可见。不过,无法从类的外部访问它。这样可以防止它被覆盖修改。这是因为在类的某些内部过程当中,我们能在不知不觉中更改变量的值。我们也许很难明白为什么代码没有按预期工作。
一旦这些完成后,我们需要告诉我们的类开始识别配置文件中的新命令。此过程是在非常具体的点上完成的,但可能会因我们所添加的内容而异。在我们的例子中,我们将按如下所示的顺序来做:
inline bool Configs(const string szInfo) { const string szList[] = { "POINTSPERTICK", "PATH" }; string szRet[]; char cWho; if (StringSplit(szInfo, '=', szRet) == 2) { StringTrimRight(szRet[0]); StringTrimLeft(szRet[1]); for (cWho = 0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[0]) break; switch (cWho) { case 0: m_PointsPerTick = StringToDouble(szRet[1]); return true; case 1: m_szPath = szRet[1]; return true; } Print("Variable >>", szRet[0], "<< undefined."); }else Print("Definition of configuratoin >>", szInfo, "<< invalid."); return false; }
注意,当所有代码的结构都得以改进时,这就容易得多。不过,我们必须小心。如果我们采取预先措施,我们将毫无问题地把我们需要的所有一切添加到代码之中。
我们要做的第一件事是把配置文件中要用到的命令名称或标签添加到顺序数据数组之内。注意,所有这些都必须用大写字母编写。我们可以令其大小写敏感,但这会让用户更难键入,以及将其放置在配置文件当中。如果您是唯一使用该系统,并打算用同一标签但具有不同值的人,那么区分大小写的系统大概是一个好主意。否则,这个想法会令整个工作复杂化。个人而言,我认为使用相同的标签来表达不同的含义只会让我们的生活更加困难。这就是为什么我不会这样做。
将标签添加到命令矩阵后,我们需要实现其功能。此刻完成恰到好处。就这么简单。由于它是链条中的第二环,并且链条从零开始,因此我们以数字 1 来表示我们正在实现该特定功能。该思路是仅指定目录名称,所以命令十分简单。最后,我们将返回 true 给调用方,指示该命令已被识别,并成功实现。
往系统里附加任何东西的顺序与所示完全相同。一旦开始这样做,我们就能使用配置文件中提供的数据。不过,有一点我忘了提,它很简单,但值得关注。在某些情况下,添加的新资源也许会导致问题出现,而实际上或许只是因为它未正确初始化。在这种情况下,每当我们添加私密全局变量时,我们都需要确保它在类构造函数中被正确初始化。您可以在下面的代码中看到这一点,其中我们正在初始化一个新变量。
C_ConfigService() :m_szPath(NULL) { }
按这样做,我们确保为尚未赋值的变量会有一个已知值。在某些状况下,这个细节可能看起来微不足道,但在其它情况下,它可以避免严重的问题,并节省时间,且被认为是良好的编程实践。完成这项工作之后,变量已在类构造函数中初始化,并且我们已明确如何基于配置文件中指定的内容为其赋值,到了使用该值的时候了。该值将仅在一个负责控制数据库加载的函数中用到。
我们看看如何实现这一点:
bool SetSymbolReplay(const string szFileConfig) { #define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo) int file, iLine; char cError, cStage; string szInfo; bool bBarsPrev; C_FileBars *pFileBars; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { Print("Failed to open the configuration file [", szFileConfig, "]. Closing the service..."); return false; } Print("Loading ticks for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; iLine = 1; cError = cStage = 0; bBarsPrev = false; while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0)) { switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: cError = (WhatDefine(szInfo, cStage) ? 0 : 1); break; case Transcription_INFO: if (szInfo != "") switch (cStage) { case 0: cError = 2; break; case 1: pFileBars = new C_FileBars(macroFileName); if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true; delete pFileBars; break; case 2: if (LoadTicks(macroFileName) == 0) cError = 4; break; case 3: if ((m_dtPrevLoading = LoadTicks(macroFileName, false)) == 0) cError = 5; else bBarsPrev = true; break; case 4: if (!BarsToTicks(macroFileName)) cError = 6; break; case 5: if (!Configs(szInfo)) cError = 7; break; } break; }; iLine += (cError > 0 ? 0 : 1); } FileClose(file); switch(cError) { case 0: if (m_Ticks.nTicks <= 0) { Print("No ticks to use. Closing the service..."); cError = -1; }else if (!bBarsPrev) FirstBarNULL(); break; case 1 : Print("Command in line ", iLine, " cannot be recognized by the system..."); break; case 2 : Print("The system did not expect the content of the line ", iLine); break; default : Print("Error in line ", iLine); } return (cError == 0 ? !_StopFlag : false); #undef macroFileName }
鉴于我们将以独特的方式同时在几个不同的地方使用它,故我选用宏定义来简化编码。所有标记为黄色之处都将完整接收宏定义中包含的代码。这大大简化了任务,因为没有必要多次编写相同的内容。这也避免了维护时在多个不同位置修改所用代码可能发生的错误。现在我们来仔细看看宏定义的作用。
#define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
还记得我们用特定值初始化了一个变量吗?当我们尝试使用该变量时,我们将准确检查它包含的值。如果这与我们在构造函数中初始化的路径相同,我们就有了一个已定义路径。如果它是在配置文件中找到的路径,我们就有不同的路径,但或多或少,我们最终会得到一个可以访问该文件的名称。
这个系统是如此通用,以至于您可以随时更改目录,而无需在已完工且编译过的系统中更改任何东西。以此方式,在更改配置文件时,我们就不必重新编译所有代码。若要更改我们打算用到的目录,我们唯一需要做的就是在配置文件中使用以下片段:
[Config] Path = < NEW PATH >
其中 <NEW PATH> 将包含新地址,从现在开始将采用配置文件中地址。这太好了,因为在操控数据库时可能包含目录结构,而这会大大减少工作量。记住,您应当系统化地组织目录中的数据,以便能更轻松地找到您所需的文件。
一旦完成之后,我们可以转入下一步,其中我们将完成一些需要实现的事情。这将在下一主题中讨论。
调整自定义品种数据
为了实现我们的订单系统,我们最初需要三个基本值:最小交易量、最小跳价值和最小跳价大小。这些值类型中目前只实现了一种,且其实现并不完全符合要求,因为也许会发生未在配置文件中设置该值的情况。这令我们创建合成品种的工作复杂化,其仅涉及模拟可能的市场走势。
如果没有所需的调整,当我们以后操控该订单系统时,其中的数据也许会不一致。我们需要确保正确配置此数据。这将令我们尝试在系统中实现某些东西时避免出现问题,毕竟代码已经相当长了。因此,我们将开始纠正错误之处,从而避免下一阶段工作中的问题。如果我们有问题,就让它们具有不同的性质。订单系统实际上不会与创建市场回放/模拟服务的服务进行交互。我们会遇到的唯一信息是图表和品种名称,仅此而已。至少这是我现在的意图。我不知道我们是否真的会成功。
对于这种场景,我们要做的第一件事就是初始化我们绝对需要的三个值。不过,它们都会设置为零。我们按部就班地研究这个问题。首先,我们需要修复我们的问题。这是在以下代码中完成的:
C_Replay(const string szFileConfig) { m_ReplayCount = 0; m_dtPrevLoading = 0; m_Ticks.nTicks = 0; Print("************** Market Replay Service **************"); srand(GetTickCount()); GlobalVariableDel(def_GlobalVariableReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol); CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX); SymbolSelect(def_SymbolReplay, true); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0.0); CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); m_IdReplay = (SetSymbolReplay(szFileConfig) ? 0 : -1); }
在此,我们将初始值设置为零,但作为福利,我们还将提供自定义品种的描述。这不是必需的,但如果您打开一个包含品种列表的窗口,并看到一个独有名称的品种,这可能会很有趣。您大概已经注意到,我们将不再使用以前存在的变量。变量会在特定位置声明,如下面的代码所示:
class C_FileTicks { protected: struct st00 { MqlTick Info[]; MqlRates Rate[]; int nTicks, nRate; bool bTickReal; }m_Ticks; double m_PointsPerTick; private : int m_File;
现在,出现此变量的所有点都应引用品种中包含和定义的值。现在我们有了新代码,但基本上这个值在贯穿回放/模拟系统中的两处被提及。第一处如下所示:
inline long RandomWalk(long pIn, long pOut, const MqlRates &rate, MqlTick &tick[], int iMode) { double vStep, vNext, price, vHigh, vLow, PpT; char i0 = 0; PpT = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); vNext = vStep = (pOut - pIn) / ((rate.high - rate.low) / PpT); vHigh = rate.high; vLow = rate.low; for (long c0 = pIn, c1 = 0, c2 = 0; c0 < pOut; c0++, c1++) { price = tick[c0 - 1].last + (PpT * ((rand() & 1) == 1 ? -1 : 1)); price = tick[c0].last = (price > vHigh ? price - PpT : (price < vLow ? price + PpT : price)); switch (iMode) { case 0: if (price == rate.close) return c0; break; case 1: i0 |= (price == rate.high ? 0x01 : 0); i0 |= (price == rate.low ? 0x02 : 0); vHigh = (i0 == 3 ? rate.high : vHigh); vLow = (i0 ==3 ? rate.low : vLow); break; case 2: break; } if ((int)floor(vNext) < c1) { if ((++c2) <= 3) continue; vNext += vStep; if (iMode == 2) { if ((c2 & 1) == 1) { if (rate.close > vLow) vLow += PpT; else vHigh -= PpT; }else { if (rate.close < vHigh) vHigh -= PpT; else vLow += PpT; } } else { if (rate.close > vLow) vLow = (i0 == 3 ? vLow : vLow + PpT); else vHigh = (i0 == 3 ? vHigh : vHigh - PpT); } } } return pOut; }
由于我们不想在多处重复相同的代码,因此我们使用局部变量来帮助我们。不过,原理是相同的:我们指的是品种内定义的值。该值引用的第二处位于 C_Replay 类之中。不过,出于实际原因,我们要做的事情与上面所示的内容略有不同,与我们创建随机游走时相反。在图表中显示和使用信息往往会降低性能,因为有太多不必要的调用。这是因为在创建随机游走期间,每根柱线将产生三次访问。
但是一旦它被创建,它就可以包含数千个跳价,所有这些都是在三次调用中创建的。这往往会在呈现和绘图过程中略微降低性能,但我们来看看在实践中这样做是如何发挥作用的。当我们使用真正的跳价文件时,即我们回放,这样的降速不会出现。这是因为当使用真实数据时,系统不需要任何额外的信息来绘制 1-分钟柱线,以及将信息传输到市场观察窗口中的跳价图表。我们在之前的两篇文章中对此进行了研究。
但是,当我们要使用 1-分钟柱线来生成跳价时,即执行模拟时,我们需要知道跳价大小,如此这些信息才可以帮助服务创建合适的走势模型。这种走势将在“市场观察”窗口中可见。但是此信息并非创建柱线所需,因为转换是在 C_FileTicks 类中执行的。
知道了这个细节,我们必须研究生成指定图表的函数,且还要检查在执行过程中将收到多少次调用。以下是模拟期间所用的函数:
inline void CreateBarInReplay(const bool bViewMetrics, const bool bViewTicks) { #define def_Rate m_MountBar.Rate[0] bool bNew; MqlTick tick[1]; static double PointsPerTick = 0.0; if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)) { PointsPerTick = (PointsPerTick == 0.0 ? SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) : PointsPerTick); if (bViewMetrics) Metrics(); m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time); def_Rate.real_volume = 0; def_Rate.tick_volume = 0; } bNew = (def_Rate.tick_volume == 0); def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close); def_Rate.open = (bNew ? def_Rate.close : def_Rate.open); def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high); def_Rate.low = (bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low); def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real; def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0); def_Rate.time = m_MountBar.memDT; CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate); if (bViewTicks) { tick = m_Ticks.Info[m_ReplayCount]; if (!m_Ticks.bTickReal) { static double BID, ASK; double dSpread; int iRand = rand(); dSpread = PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? PointsPerTick : 0 ) : 0 ); if (tick[0].last > ASK) { ASK = tick[0].ask = tick[0].last; BID = tick[0].bid = tick[0].last - dSpread; } if (tick[0].last < BID) { ASK = tick[0].ask = tick[0].last + dSpread; BID = tick[0].bid = tick[0].last; } } CustomTicksAdd(def_SymbolReplay, tick); } m_ReplayCount++; #undef def_Rate }
于此我们声明了一个静态局部变量。这是为了避免不必要的调用修复跳价大小的函数。在服务生存周期和运行时,这样的捕获只会发生一次,而变量仅在指定位置使用。故此,将其扩展到此函数之外是没有意义的。但要注意,实际使用变量的这处只在我们使用模拟模式时才可用。在回放模式下,该变量没有实际用途。
这也解决了跳价大小的问题。还有两个问题亟待解决。不过,跳价的问题尚未彻底解决。初始化存在问题。我们将在解决其它两个问题的同时解决它,因为方式将是相同的。
最后要创建的细节
问题是我们实际上应该调整什么。的确,我们可以在自定义品种中调整若干事项。但其中大多并非我们的目的所需。我们只需关注我们真正需要的东西。我们还需要并对其进行设置,如此在我们需要这些信息时,我们就能以简单而通用的方式获得它。我之所以这样说,是因为我们很快就会开始创建一个订单系统,但我不确定它是否真的会那么快发生。任何情况下,我希望我们的 EA 与回放/模拟兼容,并且适合在真实市场中使用,既可是模拟账户,亦或真实账户。为此,我们需要一些含有所需信息的组件,且与真实市场中存在的级别相同。
在这种情况下,我们需要用零值初始化它们。这可确保自定义品种的这些值与实际交易品种中的值一致。此外,将值初始化为零意味着我们可以稍后测试它们,并且它令实现和测试品种配置中可能的错误的工作变得更加容易。
C_Replay(const string szFileConfig) { m_ReplayCount = 0; m_dtPrevLoading = 0; m_Ticks.nTicks = 0; Print("************** Market Replay Service **************"); srand(GetTickCount()); GlobalVariableDel(def_GlobalVariableReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol); CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX); SymbolSelect(def_SymbolReplay, true); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); m_IdReplay = (SetSymbolReplay(szFileConfig) ? 0 : -1); }
在此,我们设置未来真正需要的那些值,就在不久。这些值是跳价大小、跳价值、和交易量(在本例中为步数)。但由于该步数通常对应于应使用的最小交易量,故在我看来设置它替代最小交易量没有任何问题。还因为在下一阶段,这个步数对我们来说更加重要。还有另一个原因:我尝试调整最小交易量,但由于某种原因我无法这样做。MetaTrader 5 简单地忽略了我们需要设置最小交易量的事实。
一旦完成之后,我们还需要做些事情,并检查这些值是否已真的被初始化。这是在下一段代码中完成的:
bool ViewReplay(ENUM_TIMEFRAMES arg1) { #define macroError(A) { Print(A); return false; } u_Interprocess info; if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) macroError("Configuração do ativo não esta completa, falta declarar o tamanho do ticket."); if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) macroError("Configuração do ativo não esta completa, falta declarar o valor do ticket."); if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) macroError("Configuração do ativo não esta completa, falta declarar o volume mínimo."); if (m_IdReplay == -1) return false; if ((m_IdReplay = ChartFirst()) > 0) do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) { ChartClose(m_IdReplay); ChartRedraw(); } }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0); Print("Aguardando permissão do indicador [Market Replay] para iniciar replay ..."); info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); ChartRedraw(m_IdReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); GlobalVariableTemp(def_GlobalVariableIdGraphics); GlobalVariableSet(def_GlobalVariableIdGraphics, info.u_Value.df_Value); while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750); return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != "")); #undef macroError }
为了避免不必要的重复,我们将使用宏定义。它将显示一条错误消息,并以系统故障消息结束。在此,我们逐一检查必须在配置文件中声明和初始化的值。如果其中任何一个尚未初始化,系统将通知用户正确配置自定义品种。不这样的话,回放/模拟服务将无法继续起作用。从这一刻起,可以认为它能起作用,并能为订单系统提供必要的数据,在本例中是正在创建的 EA,以便正确建模或回放行情。这将令我们能够模拟发送订单。
不错,但为了初始化这些值,我们需要对系统进行一些添加,如下所示:
inline bool Configs(const string szInfo) { const string szList[] = { "PATH", "POINTSPERTICK", "VALUEPERPOINTS", "VOLUMEMINIMAL" }; string szRet[]; char cWho; if (StringSplit(szInfo, '=', szRet) == 2) { StringTrimRight(szRet[0]); StringTrimLeft(szRet[1]); for (cWho = 0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[0]) break; switch (cWho) { case 0: m_szPath = szRet[1]; return true; case 1: CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, StringToDouble(szRet[1])); return true; case 2: CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, StringToDouble(szRet[1])); return true; case 3: CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, StringToDouble(szRet[1])); return true; } Print("Variable >>", szRet[0], "<< undefined."); }else Print("Definition of configuratoin >>", szInfo, "<< invalid."); return false; }
往系统里添加内容非常简单。在此,我们加入了两个新值,只需编辑任何品种的回放或模拟配置文件即可。这是一个跳价值,它将调用一次函数生成所需的相应值,以及一个步数值,该值也将调用内部函数来调整该值。任何其它附加内容都将在后续步骤中进行。
后记
我还没有测试这些值合适与否。因此,在编辑配置文件时要小心,以免在使用订单系统时出现错误。
无论如何,您可以使用附加的自定义品种检查工作的进展情况。
重要提示:尽管该系统实际上可以正常运行,但这并不完全正确。由于目前它还无法执行外汇数据的回放或模拟。因为外汇市场用到了一些该系统还无法处理的东西。尝试这样做会导致系统数组超界错误,无论是回放亦或模拟模式。但我正在努力修复,以便能够处理外汇市场数据。
在下一篇文章中,我们将开始研究这个话题:外汇。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11125
注意: 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.


