概述
我认为从本系列的前几篇文章中可以清楚地看出,赫兹量化交易软件需要实现一些额外的要点。更好地组织工作是绝对必要的,尤其是一些深入的改进。如果您计划仅用回放/模拟系统来操控一种资产,那么您就不需要我们将要实现的许多东西。您可以把它们放在一边 — 我的意思是它们不必出现在配置文件之中。
不过,您很可能不只用到一种资产,而是若干种不同资产,甚至是相当大的数据库。在这种情况下,我们就需要把事情组织好,由此需要实现额外的代码来达成该目标,尽管在某些非常特殊的情况下,赫兹量化交易软件能简单地使用源代码中已有的内容,不过是以隐形方式。这只需要将其放到明处。
我总是喜欢把事情安排得井井有条,我猜很多人也会这样想,并尝试这样做。知晓并理解如何实现此功能会很好。此外,如果您需要研究或分析特定资产的特定参数,您肯定要学习如何向系统添加新参数。
在此,赫兹量化交易软件要做好准备,如此当我们需要往代码里添加新函数时,就能顺滑轻松地发生。当前代码还不能涵盖或处理那些显著推进过程所必需的事情。我们需要将所有东西都结构化,以便能够以最小的工作量实现某些事情。如果我们正确地做好所有事情,我们就能得到一个真正通用的系统,可以轻松地适应任何需要处理的状况。这些方面之一将是下一篇文章的主题。幸运的是,感谢最后两篇文章展示了如何向市场观察窗口添加跳价,事情总体上正按计划前进。如果您错过了这些文章,可以按以下链接去访问它们:开发回放系统 — 市场模拟(第 17 部分):调价和更多跳价(I) ,和开发回放系统 — 市场模拟(第 18 部分):跳价和更多跳价(II)。这两篇文章提供的宝贵信息,都与我们后续文章中将要做的事情有关。
不过,仍然缺少一些非常具体的细节,这些都将在本文中实现。此外,还有其它十分复杂的问题,需要单独的文章来解释如何梳理并解决这些问题。我们来看看在本文中现在开始实现系统。我们将从改进所用的数据组织结构开始。
添加图片注释,不超过 140 字(可选)
实现目录系统
此处的问题不在于我们是否真的需要实现这个系统,而是我们为什么要实现它。在当前开发阶段,赫兹量化交易软件可以使用目录系统。不过,我们将不得不做更多的工作来实现回放/模拟服务。我的意思是,不仅简单地将新变量添加到配置文件当中,还有更多工作。为了明白我在说什么,请看下面的图片:
添加图片注释,不超过 140 字(可选)
图例 01 – 访问当前系统中目录的途径。
添加图片注释,不超过 140 字(可选)
图例 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> 将包含新地址,从现在开始将采用配置文件中地址。这太好了,因为在操控数据库时可能包含目录结构,而这会大大减少工作量。记住,您应当系统化地组织目录中的数据,以便能更轻松地找到您所需的文件。
一旦完成之后,我们可以转入下一步,其中我们将完成一些需要实现的事情。这将在下一主题中讨论。