开发回放系统 — 市场模拟(第 11 部分):模拟器的诞生(I)
概述
到目前为止,包括上一篇文章 开发回放系统 — 市场模拟(第 10 部分):仅使用真实数据回放,我们所做的一切都涉及真实数据,这意味着我们都用的是实际交易票据。 这可让走位精准,且易于创建,因为我们不必担心如何收集信息。 我们所要做的就是将交易票据转换为 1-分钟柱线,剩下的交给 MetaTrader 5 平台。
然而,我们现在面临着另一项更艰巨的任务。
规划
许多人可能认为规划很容易,特别是,由于它事关转换柱线,即始终把 1-分钟柱线转换为票据(我们稍后会解释)。 然而,模拟比表面看要复杂得多。 主要问题在于我们尚未清晰地理解票据的实际行为,就去创建 1-分钟柱线。 我们只有柱线,和一些它的有关信息,但我们并不知道柱线是如何形成的。 我们将采用 1-分钟柱线,因为它们所需的难度最小。 如果您能创造一个与真实事物非常相似的复杂走位,那么您就能够再现一些非常接近真实事物的东西。
这个细节似乎并不那么重要,因为我们通常会在市场上看到锯齿形的走位。 无论走位如何复杂,这一切都归结为在 OHCL 点之间创建锯齿形。 它始自柱线的开盘那一刻,且需不少于 9 个走位来创造这个内部的锯齿形。 它总是在柱线收盘时结束,并在下一根柱线上重复该过程。 MetaTrader 5 策略测试器采用相同的逻辑。 更多详细信息,请参阅真实与生成的基差:算法交易。 我们将从这个策略开始。 虽然对于我们的目的来说并不理想,但它将为开拓更多适合的方式提供一个起点。
就我而言,测试策略器不太适合回放/模拟系统,因为在策略测试器中,时间问题并不是最重要的。 这就是,没必要以这种方式创建和复现 1-分钟柱线,因为其长度本来就是 1-分钟。 事实上,它甚至更简略,在于它与现实时间并不对应。 如果是这样的话,那么测试策略就变得不可能。 想象一下,依据跨越几天甚至几年的柱运行测试,如果每根柱线复现时间与实际不同。 这将是一项不可能完成的任务。 然而,对于回放/模拟系统,我们正在寻找一种不同的行动。 我们希望按照 1-分钟间隔创建一根 1-分钟柱线,尽可能接近这个目标。
准备奠基
我们的重点将完全放在回放/模拟服务代码上。 此时无需担心其它方面。 故此,我们将开始修改 C_Replay 类的代码,尝试尽可能地优化我们已经开发完成并测试过的东西。 下面是类中出现的第一个过程:
inline bool CheckFileIsBar(int &file, const string szFileName) { string szInfo = ""; bool bRet; for (int c0 = 0; (c0 < 9) && (!FileIsEnding(file)); c0++) szInfo += FileReadString(file); if ((bRet = (szInfo == def_Header_Bar)) == false) { Print("File ", szFileName, ".csv is not a file with bars."); FileClose(file); } return bRet; }
此处的目标是柱线读取函数,这些测试是为了判定指定文件是否为预览柱线文件。 这是必要的,当使用相同代码来判定柱线文件是否符合我们所需时,如此可避免重复代码。 在此状况下,这些柱线将不会用作预览柱线。 它们将被转换为模拟票据,以便在交易系统中使用。 有基于此,我们引入了另一个函数:
inline void FileReadBars(int &file, MqlRates &rate[]) { rate[0].time = StringToTime(FileReadString(file) + " " + FileReadString(file)); rate[0].open = StringToDouble(FileReadString(file)); rate[0].high = StringToDouble(FileReadString(file)); rate[0].low = StringToDouble(FileReadString(file)); rate[0].close = StringToDouble(FileReadString(file)); rate[0].tick_volume = StringToInteger(FileReadString(file)); rate[0].real_volume = StringToInteger(FileReadString(file)); rate[0].spread = (int) StringToInteger(FileReadString(file)); }
它将从指定文件里逐行读取已有柱线的数据。 我想您在理解这段代码时不会遇到任何困难。 继续这个准备阶段,此处是另一个函数:
inline bool OpenFileBars(int &file, const string szFileName) { if ((file = FileOpen("Market Replay\\Bars\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { if (!CheckFileIsBar(file, szFileName)) return false; return true; } Print("Falha ao acessar ", szFileName, ".csv de barras."); return false; }
我们的系统现在已经完全集中化,能提供对柱线的标准访问:我们即可把它们用作预览柱线时,亦可把它们用于模拟,并转换为票据表示。 因此,之前加载预览柱线的函数也必须修改,如下所示:
bool LoadPrevBars(const string szFileNameCSV) { int file, iAdjust = 0; datetime dt = 0; MqlRates Rate[1]; if (OpenFileBars(file, szFileNameCSV)) { Print("Loading preview bars for Replay. Please wait...."); while ((!FileIsEnding(file)) && (!_StopFlag)) { FileReadBars(file, Rate); iAdjust = ((dt != 0) && (iAdjust == 0) ? (int)(Rate[0].time - dt) : iAdjust); dt = (dt == 0 ? Rate[0].time : dt); CustomRatesUpdate(def_SymbolReplay, Rate, 1); } m_dtPrevLoading = Rate[0].time + iAdjust; FileClose(file); return (!_StopFlag); } m_dtPrevLoading = 0; return false; }
这个下载函数的工作方式并无变化,尽管现在有更多的调用。 从以前的函数中提取一部分,并在新位置加以运用,为我们提供了更高的安全性,因为所有代码都已经过测试。 以这种方式,我们只需要担心新函数。 现在地基已经准备就绪,我们需要在配置文件中添加新内容呢。 该函数旨在判定哪些柱线文件可用于模拟票据。 为此,我们需要添加一个新定义:
#define def_STR_FilesBar "[BARS]" #define def_STR_FilesTicks "[TICKS]" #define def_STR_TicksToBars "[TICKS->BARS]" #define def_STR_BarsToTicks "[BARS->TICKS]"
这样我们就可以运行一个简单的测试,这正是我们开始进行模拟所需要的。
bool SetSymbolReplay(const string szFileConfig) { #define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("An error occurred in line %d", iLine)), "Market Replay", MB_OK); return false; } int file, iLine; string szInfo; char iStage; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open the\nconfiguration file.", "Market Replay", MB_OK); return false; } Print("Loading data for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; iStage = 0; iLine = 1; while ((!FileIsEnding(file)) && (!_StopFlag)) { switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: if (szInfo == def_STR_FilesBar) iStage = 1; else if (szInfo == def_STR_FilesTicks) iStage = 2; else if (szInfo == def_STR_TicksToBars) iStage = 3; else if (szInfo == def_STR_BarsToTicks) iStage = 4; else macroERROR(StringFormat("%s is not recognized in the system\nin line %d.", szInfo, iLine)); break; case Transcription_INFO: if (szInfo != "") switch (iStage) { case 0: macroERROR(StringFormat("Command not recognized in line %d\nof the configuration file.", iLine)); break; case 1: if (!LoadPrevBars(szInfo)) macroERROR(""); break; case 2: if (!LoadTicksReplay(szInfo)) macroERROR(""); break; case 3: if (!LoadTicksReplay(szInfo, false)) macroERROR(""); break; case 4: if (!LoadBarsToTicksReplay(szInfo)) macroERROR(""); break; } break; }; iLine++; } FileClose(file); return (!_StopFlag); #undef macroERROR }
看看,往代码里添加新函数是多么容易。 加上该测试,事实上就给了我们分析更多方面的机会。 从这里,我们可以查看到在其它状况下观察到的相同类型行为。 在此阶段定义的任何文件,都会按照转换票据的柱线文件对待,这是由此调用完成的。
再次重申,只要有可能,我们应当避免编写不必要的代码。 建议重用以前测试过的代码。 这正是我们至今所做的。 不过,我们很快就会开始一个新主题,尽管这将是另一篇文章的主题。 但在此之前,理解一个基本面很重要。
实现前的几点思考
在实现转换系统之前,有一点需要考虑。 您知道真正存在多少种不同类型的柱线配置吗? 尽管许多人认为有很多类型,但实际上我们可以将所有可能的配置归纳为四种。 如下图所示:
为什么这与我们有关? 这确实是相关的,因为它决定了有多少个选项需要我们去实现。 如果我们不明白只有这四个选项的事实,那么我们就要冒错过一些选项的风险,或者相反,创建不必要的案例类型。 再次,我想强调没有办法创建一个完美的模拟模型,来重建柱线。 最可能达成的是,针对导致该特定柱线形成的实际走位,或多或少的评估准确度。
关于第二种类型有一些细节,如图所示,柱线主体只能放置在顶部。 不过,这个事实不会影响我们即将实现的系统。 同样,柱线是否代表卖出或买入交易并不重要;实现将保持不变。 唯一的细微差别在于我们应该朝着哪个初始方向前进。 以这种方式,我们需要实现的案例数量就可以最小化。 但除了示意图中呈现的案例之外,我们还需要了解一件事:我们真正需要创建的票据最小数量是多少? 这也许会让一些人感到困惑,但对于实现回放/模拟系统,甚至策略测试器的人来说,这一切都具有意义。
我们想一想:在任何系统中只用到 1 次票据是不切实际的,因为这只代表买入或卖出交易,而不是走位本身。 因此,我们可以排除这种可能性。 我们至少要遇到两次票据,象征着开盘点和收盘点。 虽然这看似合乎逻辑,但我们也不会得到任何真正的走位,因为我们只需要生成一个基差表示柱线开盘,而第二次是收盘。
注意:我们并非尝试生成票据,事实上,我们是想创建模拟一根柱线的走位。 我们将在将来的文章中进一步探讨这些主题,但首先我们需要开发一个基本系统。
因此,最小票据数量为 3 次。 这样,1-分钟柱线可能反映了之前示意图中观察到的一些配置。 不过,请注意,至少存在 3 次票据并不意味着价格正好 1 个基差上涨,和 1 个基差下跌,或者 3 个基差同上或同下。 若是柱线创建时缺乏流动性,来自这 1 次跳价的走位也许不同。
重要提示:读者可能会对此处用到的一些术语感到困惑。 为了避免误解,我们要澄清一下:当我提到 TICKET(票据) 这个词时,我实际上指的是一个交易事件,即以指定价格买入或卖出资产的事件。 关于 TICK(基差) 一词,我的意思是相对于交易价格的最小偏差。 为了理解这种区别,您需要考虑以下几点:股票市场的 1 个基差成本为 0.01 点,美元期货的 1 个基差成本为 0.5 点,指数期货的 1 个基差的成本为 5 点。
虽然这可能会在某些方面令事情变得更加困难,因为走位模拟不再反映确切的现实,而是一个理想化的走位,重要的是提到这一事实,请记住,在许多情况下,一个模拟系统若用 1-分钟柱线,将无法准确再现市场当中实际已发生、或正在发生的事情。 故此,最好始终使用最短的时间帧。 由于该时间帧为 1-分钟,您就应始终用它。
也许您还不明白依据柱线创建票据方法的真正问题。 但请记住以下几点:若在真实市场中,1-分钟柱线以某个价格开盘。 由于缺乏流动性,价格跳涨 3 个基差,一段时间后下跌 1 个基差,然后当它在最后一个位置收盘时,最终情形如下:
上图可能令人困惑,您可能没有正确理解它。 它代表以下信息: 在左边一角,我们看到以基差为单位的实际走位。 短水平条代表每个基差。 圆圈表示资产在两次票证之间实际停止的价格。 绿线表示价格上涨。 请注意,在某些情况下,在某些基差上没有交易,但在分析 OHCL 值时,我们看不到任何明显的基差峰值。 通过仅用柱线烛条模拟走位,我们可以看到下图中显示的内容。
蓝线代表模拟的走位。 在这种情况下,我们将遍历全部基差,无论在实时交易期间实际发生了什么。 由此,请始终记住,建模与使用真实数据并不相同。 无论建模系统多么复杂,它永远无法准确反映现实。
实现基本转换系统
如前所述,首先要做的是确定价格变动的大小。 为此,我们必须在配置文件中包含一些额外元素。 它们必须能由 C_Replay 类识别。 因此,我们需要向该类中添加一些定义和额外代码。 我们将从以下几行开始。
#define def_STR_FilesBar "[BARS]" #define def_STR_FilesTicks "[TICKS]" #define def_STR_TicksToBars "[TICKS->BARS]" #define def_STR_BarsToTicks "[BARS->TICKS]" #define def_STR_ConfigSymbol "[CONFIG]" #define def_STR_PointsPerTicks "POINTSPERTICK" #define def_Header_Bar "<DATE><TIME><OPEN><HIGH><LOW><CLOSE><TICKVOL><VOL><SPREAD>" #define def_Header_Ticks "<DATE><TIME><BID><ASK><LAST><VOLUME><FLAGS>" #define def_BarsDiary 540
此行定义了一个字符串,其将识别我们将要处理的配置数据。 它还提供了我们从现在开始可以定义的第一个配置。 再次,我们需要向系统添加几行代码,如此可以解释和应用这些设定,但所添加内容相对简单。 接下来,我们看看需要完成什么:
bool SetSymbolReplay(const string szFileConfig) { #define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("An error occurred in line %d", iLine)), "Market Replay", MB_OK); return false; } int file, iLine; string szInfo; char iStage; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open the\nconfiguration file.", "Market Replay", MB_OK); return false; } Print("Loading data for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; iStage = 0; iLine = 1; while ((!FileIsEnding(file)) && (!_StopFlag)) { switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: if (szInfo == def_STR_FilesBar) iStage = 1; else if (szInfo == def_STR_FilesTicks) iStage = 2; else if (szInfo == def_STR_TicksToBars) iStage = 3; else if (szInfo == def_STR_BarsToTicks) iStage = 4; else if (szInfo == def_STR_ConfigSymbol) iStage = 5; else macroERROR(StringFormat("%s is not recognized in the system\nin line %d.", szInfo, iLine)); break; case Transcription_INFO: if (szInfo != "") switch (iStage) { case 0: macroERROR(StringFormat("Command not recognized in line %d\nof the configuration file.", iLine)); break; case 1: if (!LoadPrevBars(szInfo)) macroERROR(""); break; case 2: if (!LoadTicksReplay(szInfo)) macroERROR(""); break; case 3: if (!LoadTicksReplay(szInfo, false)) macroERROR(""); break; case 4: if (!LoadBarsToTicksReplay(szInfo)) macroERROR(""); break; case 5: if (!Configs(szInfo)) macroERROR(""); break; } break; }; iLine++; } FileClose(file); return (!_StopFlag); #undef macroERROR }
此处,我们指出来自下一行的所有信息,都将在分析的第 5 阶段由系统处理。 当触发信息项,并在步骤 5 中捕获时,会调用该过程。 为了简化此过程的描述,下面我们将讲述在第 5 阶段调用的过程。
inline bool Configs(const string szInfo) { string szRet[]; if (StringSplit(szInfo, '=', szRet) == 2) { StringTrimRight(szRet[0]); StringTrimLeft(szRet[1]); if (szRet[0] == def_STR_PointsPerTicks) m_PointsPerTick = StringToDouble(szRet[1]); else { Print("Variable >>", szRet[0], "<< not defined."); return false; } return true; } Print("Definition of configuration >>", szInfo, "<< is invalid."); return false; }
首先,我们捕获信息项,并将内部变量名称与其数值隔离开来,供我们以后所用。 这是由用户在回放/模拟配置文件中定义的。 此操作的结果将为我们提供两段信息片:第一个是定义的变量名称,第二个是其数值。 噶值的类型可能因变量而异,但它对用户是完全透明的。 您不必担心类型是字符串型、双精度型还是整数型。 在代码中会选择对应类型。
在使用该数据之前,必须删除与基本信息无关的所有内容。 典型情况,该元素是某种内部格式类型,是为了便于用户读取或写入配置文件。 任何无法理解和应用的东西都被视为出错。 这会导致我们返回 false,顺便又会导致服务终止。 当然,稍后会在 MetaTrader 5 平台中报告原因。
所以我们的配置文件如下。 请记住,这只是一个示例配置文件:
[Config] PointsPerTick = 5 [Bars] WIN$N_M1_202112060900_202112061824 WIN$N_M1_202112070900_202112071824 [ Ticks -> Bars] [Ticks] [ Bars -> Ticks ] WIN$N_M1_202112080900_202112081824 #End of the configuration file...
在继续之前,您需要在系统上进行一些最终设置。 这是至关重要的。 我们需要调试系统,确保柱线机制转换为模拟票据模型。 所需的更改在以下代码中高亮显示:
inline int Event_OnTime(void) { bool bNew; int mili, iPos; u_Interprocess Info; static MqlRates Rate[1]; static datetime _dt = 0; datetime tmpDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time); if (m_ReplayCount >= m_Ticks.nTicks) return -1; if (bNew = (_dt != tmpDT)) { _dt = tmpDT; Rate[0].real_volume = 0; Rate[0].tick_volume = 0; } mili = (int) m_Ticks.Info[m_ReplayCount].time_msc; do { while (mili == m_Ticks.Info[m_ReplayCount].time_msc) { Rate[0].close = m_Ticks.Info[m_ReplayCount].last; Rate[0].open = (bNew ? Rate[0].close : Rate[0].open); Rate[0].high = (bNew || (Rate[0].close > Rate[0].high) ? Rate[0].close : Rate[0].high); Rate[0].low = (bNew || (Rate[0].close < Rate[0].low) ? Rate[0].close : Rate[0].low); Rate[0].real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real; bNew = false; m_ReplayCount++; } mili++; }while (mili == m_Ticks.Info[m_ReplayCount].time_msc); Rate[0].time = _dt; CustomRatesUpdate(def_SymbolReplay, Rate, 1); iPos = (int)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks); GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value); if (Info.s_Infos.iPosShift != iPos) { Info.s_Infos.iPosShift = (ushort) iPos; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); } return (int)(m_Ticks.Info[m_ReplayCount].time_msc < mili ? m_Ticks.Info[m_ReplayCount].time_msc + (1000 - mili) : m_Ticks.Info[m_ReplayCount].time_msc - mili); }
此处一个有趣的细节:您可以关闭可视化,从而观看图表上的走位如何发生。 我们正在谈论禁用函数,它排除了模拟票据中存在的秒数值。 在这种情况下,每个模拟票据将在图表上表现为一根柱线。
虽然这看起来有点不寻常,但它将帮助我们了解正在发生的事情,而无需写入日志文件。 对于此项,我们必须删除与秒数对应的数值。 如果我们不这样做,那么每个模拟票据都会生成一根柱线,而最终我们并不需要它。 我们希望 1-分钟柱线看起来就像它们是真实的一样。
最后,我们进入实现阶段
在我们继续代码之前,我们看看将在本文中讲述的走位。 在将来,我将向读者展示如何令其更复杂,但最重要的是首先让它能够正常工作。 您可以在下面看到这种走位将如何发生:
虽然这也许看起来很简单,但我们会将柱线转换为模拟票据走位。 因为走位从来都不是线性的,而是一种锯齿形,所以我决定由三个这样的振荡来组成一个走位。 如果您愿意,可以增加此数字。 在以后的文章中,我将向您展示如何将这种基本技术转变为更复杂的东西。
现在我们知道了这个走位会是什么,我们可以进入代码了。 创建转换系统所需的第一个函数如以下代码所示:
bool LoadBarsToTicksReplay(const string szFileNameCSV) { //#define DEBUG_SERVICE_CONVERT int file, max; MqlRates rate[1]; MqlTick tick[]; if (OpenFileBars(file, szFileNameCSV)) { Print("Converting bars to ticks. Please wait..."); ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray); ArrayResize(m_Ticks.Rate, def_BarsDiary); ArrayResize(tick, def_MaxSizeArray); while ((!FileIsEnding(file)) && (!_StopFlag)) { FileReadBars(file, rate); max = SimuleBarToTicks(rate[0], tick); for (int c0 = 0; c0 <= max; c0++) { ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray); m_Ticks.Info[m_Ticks.nTicks++] = tick[c0]; } } FileClose(file); ArrayFree(tick); #ifdef DEBUG_SERVICE_CONVERT file = FileOpen("Infos.txt", FILE_ANSI | FILE_WRITE); for (long c0 = 0; c0 < m_Ticks.nTicks; c0++) FileWriteString(file, StringFormat("%s.%03d %f --> %f\n", TimeToString(m_Ticks.Info[c0].time, TIME_DATE | TIME_SECONDS), m_Ticks.Info[c0].time_msc, m_Ticks.Info[c0].last, m_Ticks.Info[c0].volume_real)); FileClose(file); #endif return (!_StopFlag); } return false; }
虽然看起来很简单,但此函数是负责创建模拟的第一步。 看看它,我们至少可以稍微明白系统是如何执行模拟的。 重点注意的是,与策略测试器不同,在使用服务时,此处每根分钟柱线的模拟大约需要一分钟,直到它全部显示在屏幕上。 这是有意为之的。 因此,如果此处的目标是测试策略,我建议您使用 MetaTrader 5 平台上提供的工具,并且仅将本工具(我们展示的开发项目)用于手动训练和测试。
在未来,或许有些方面将不复存在,但此时此刻它是至关重要的。 这是指我们目前正在讨论的定义,它允许您生成模拟票据数据文件,以便分析模拟系统中存在的复杂程度。 重点注意的是,创建的文件将仅包含分析所需的数据,而不包含其它附加、或不必要的信息。
该函数的其余部分非常直观,因为代码之前就已经存在。 现在我们有一个调用,我们稍后会更详细地查看。 调用模拟票据之后,将执行一个小循环,将票据保存在数据库之中。 它很容易理解,不需要任何进一步的讲解。
inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[]) { int t0 = 0; long v0, v1, v2, msc; m_Ticks.Rate[++m_Ticks.nRate] = rate; Pivot(rate.open, rate.low, t0, tick); Pivot(rate.low, rate.high, t0, tick); Pivot(rate.high, rate.close, t0, tick, true); v0 = (long)(rate.real_volume / (t0 + 1)); v1 = 0; msc = 5; v2 = ((60000 - msc) / (t0 + 1)); for (int c0 = 0; c0 <= t0; c0++, v1 += v0) { tick[c0].volume_real = (v0 * 1.0); tick[c0].time = rate.time + (datetime)(msc / 1000); tick[c0].time_msc = msc % 1000; msc += v2; } tick[t0].volume_real = ((rate.real_volume - v1) * 1.0); return t0; }
上述功能令任务增加了一点困难,故此也令其更有趣。 与此同时,它帮助我们以更有条理、和更易于管理的方式将所有必要的结构组合在一起。 事实上,为了降低此函数的复杂性,我创建了另一个被调用三次的过程。 如果您阅读文档 真实和生成的基差 — 算法交易,您会注意到其中系统被调用不止三次,而是四次。 如果需要,您可以加上更多次调用,但如前所述,我将展示一种增加此系统复杂性的方法,而无需再添加其它 Pivot 过程调用。
关于前面提到的程序,调用三次 Pivot 之后,我们将获得一些模拟票据,这取决于切分如何执行。 幸亏这样,我们现在可以对 1-分钟柱线数据进行小幅修正,从而允许我们以某种方式运用原始数据。 第一步是对实际交易量进行简单切分,以便每个模拟基差都包含总交易量的一小部分。 然后,我们对每个模拟基差的时间进行小幅调整。 在定义了所用的分数之后,我们可以进入一个循环,并确保每个分数都存储在相应·的票据之中。 在目前,我们将坚持这样一个事实,即该系统必须能真正工作。 虽然上面的函数可以改进很多,如此它会让事情变得更有趣。 与时间不同,必须校正交易,以便保持与原始交易量相同。 由于这个细节,我们在这个过程中进行了最后的计算,并进行了修正,如此令 1-分钟柱线的最终成交量与初始成交量相同。
现在我们来看一下本文中的最后一个函数,它将根据上述代码提供的数值和参数创建一个轴点。 重点注意的是,这些值可以根据您的兴趣进行调整,但必须注意确保后续函数能正常工作。
//+------------------------------------------------------------------+ #define macroCreateLeg(A, B, C) if (A < B) { \ while (A < B) { \ tick[C++].last = A; \ A += m_PointsPerTick; \ } \ } else { \ while (A > B) { \ tick[C++].last = A; \ A -= m_PointsPerTick; \ } } inline void Pivot(const double p1, const double p2, int &t0, MqlTick &tick[], bool b0 = false) { double v0, v1, v2; v0 = (p1 > p2 ? p1 - p2 : p2 - p1); v1 = p1 + (MathFloor((v0 * 0.382) / m_PointsPerTick) * m_PointsPerTick * (p1 > p2 ? -1 : 1)); v2 = p1 + (MathFloor((v0 * 0.618) / m_PointsPerTick) * m_PointsPerTick * (p1 > p2 ? -1 : 1)); v0 = p1; macroCreateLeg(v0, v2, t0); macroCreateLeg(v0, v1, t0); macroCreateLeg(v0, p2, t0); if (b0) tick[t0].last = v0; } #undef macroCreateLeg //+------------------------------------------------------------------+
上述功能在操作上很简单。 尽管它的计算可能看起来有些奇怪,但在查看创建中轴的数值时,您会注意到我们总是尝试使用第一条和第三条斐波那契线来设置中轴。 首先,重点注意的是,中轴是向上还是向下并无所谓;该函数将执行相应计算。 然后碰到的一个方面,对那些缺乏编程知识的人来说可能会感到困惑:MACRO(宏)。 使用宏的原因是,使用宏能更容易创建中轴的一部分。 但您也可以为此创建一个函数。 事实上,如果我们使用纯 C++,这个宏可能会有完全不同的代码。 但在,由 MQL5 创建时,这样做可作为一种变通方式。
在声明它的区域中使用宏比嵌入代码要高效得多。
结束语
我们应该始终优先考虑易于阅读和理解的代码,而不是当需要调整或修改时,迫使我们花费数小时来弄清楚其行为的代码。 本文到此结束。 下面的视频展示了当前开发阶段的成果,它是用文章所附的内容创建的。
不过,我想提请注意使用模拟票据时出现的问题。 这涉及位移系统,或者搜索位置与回放服务所处位置不同。 仅当您使用模拟票据时,才会出现此问题。 在下一篇文章中,我们将解决和修复此故障,并进行其它改进。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10973