English Русский Español Deutsch 日本語 Português
preview
开发回放系统 — 市场模拟(第 20 部分):外汇(I)

开发回放系统 — 市场模拟(第 20 部分):外汇(I)

MetaTrader 5测试者 | 19 三月 2024, 11:10
307 0
Daniel Jose
Daniel Jose

概述

在上一篇文章“开发回放系统 — 市场模拟(第 19 部分):必要的调整”中,我们实现了某些更紧迫的事情。然而,虽然本系列的重点从一开始就立足于股票市场,但我也想尝试涵盖外汇市场。我最初对外汇缺乏兴趣的原因是,由于在这个市场里持续发生交易,故没有必要利用回放/模拟进行测试或训练。

为此,您简单地使用模拟账户即可。然而,这个市场有一些独特的问题,在股票市场里是无法复现的。出于这个原因,展示如何对系统进行必要的修正,从而令系统适应其它类型的市场(例如加密资产)就变得有趣了。

因此,MetaTrader 5 平台的多功能性和适用性将变得显而易见,其应用范围比其创建者的最初提议更宏大。限制您能力的,仅在于您对特定市场的想象力和知识。


学习一些关于外汇的知识

本文的最初目标不是涵盖外汇交易的所有可能性,而更是出于适配系统,如此您就至少可以执行一次市场回放。我们把模拟留待其它时刻。不过,如果我们没有跳价而仅有柱线的话,稍加努力,我们就可以模拟外汇市场中可能发生的交易。直到我们研究如何适配模拟器之前,情况一直如此。不经修改就尝试在系统内处理外汇数据会导致一系列错误。尽管我们试图避免此类错误,但它们终会发生。不过,它们是可以被克服的,从而为外汇市场创建一个回放系统。但为此,我们将不得不进行一些调整,并改变一些我们迄今为止正在致力的概念。我认为这是值得的,因为它能令系统更加灵活地处理更多奇异的数据。

文章附件包含一个外汇品种(货币对)。这些都是真实的跳价,故我们可以直观形势。务须质疑,外汇市场在模拟和回放方面不是轻而易举的。尽管我们看到相同的基本信息类型,但外汇有其特定的特征。因此,观察和分析它极其有趣。

这个市场具有某些特定特性。为了实现回放系统,我必须解释其中的一些特性,如此您就明白我们在谈论什么,以及了解其它市场是如此有趣。


交易是如何执行的

在外汇市场中,交易通常发生在出价(BID)和要价(ASK)值之间没有实际点差(spread)的情况下。在大多数情况下,这两个值也许是相同的。但这怎么可能呢?它们怎么可能是相同的?与股票市场不同,在股票市场中,出价和要价之间总是存在点差,而在外汇市场情况并非如此。虽然有时有点差,且有时点差会很高,但通常出价和要价值可以相同。对于那些来自股市,并想入行外汇的人来说,这可能会感到困惑,因为交易策略通常需要重大改变。

还应该指出的是,外汇市场的主要参与者是央行;那些在 B3(巴西证券交易所)工作的人已经看到,并非常清楚央行有时会对美元做什么。出于这个原因,许多人由于担心央行可能干预该货币而避免交易这种资产,因为这可能会令先前获胜的持仓迅速翻转从而变成大输家。在此期间,许多没有经验的交易者经常破产,在某些情况下甚至面临来自证券交易所和股票经纪商的诉讼。在央行没有事先警告的情况下就施行干预,这很可能发生,对持仓者也毫不留情。

不过,对我们来说,这无所谓:我们感兴趣的是程序本身。因此,在外汇市场中,价格的显示基于出价值,如图例 01 所示。


图例 01 

图例 01:外汇市场的图表显示

这与使用最后执行交易价格的 B3 显示系统有何不同呢,例如,可在下面的图例 02 中看到,其中我们有美元期货合约的数据(撰写本文时采集)。

图例 02 

图例 02:在巴西证券交易所(B3)交易的迷你美元合约。

开发回放/模拟系统是为了促进此类分析的使用。也就是,当我们使用最后交易价格时,数据在跳价文件中的布局方式会有所不同。不仅如此,甚至在跳价文件或 1-分钟柱线中实际可用的信息类型上,我们也会发现很大不同。由于这些变化,我们现在将只关注如何执行重放,因为模拟涉及其它更复杂的问题。然而,正如本文开头提到的:可以使用 1-分钟柱线数据来模拟交易过程中可能发生的情况。不光只说理论,我们来研究一下 B3(即巴西证券交易所)情况下外汇和股票市场之间的信息差异,回放/模拟系统最初是为此开发的。在图例 03 中,我们有关于其中一个外汇货币对的信息。

图例 03

图例 03:外汇市场真实交易信息

图例 04 显示了相同类型的信息,但这次来自在 B3(巴西证券交易所)交易的迷你美元期货合约之一。

图例 04

图例 04:B3 中的真实报价信息

这是完全不同的。外汇市场没有最后价格或交易量。在 B3 上,这些值都可用,许多交易模型都用到交易量和最后交易价格。到目前为止所说的一切都只是为了表明,该系统的构建方式不允许它在进行一些重大修改之前服务于其它类型的市场。我考虑过从市场的角度来拆分这个问题,但出于某种原因,这是不切实际的。若从编程的角度来看,因为这种拆分会大大简化编程;但若从可用性的角度来看,我们始终要适配一个或另一个市场。我们能做的就是尝试找到一个折中立场,但不经努力肯定做不到。我将尽可能把这些困难最小化,因为我不想也不打算从头开始重新创建整个系统。


开始实现涵盖外汇

我们要做的第一件事就是修复浮点数字系统。但是在上一篇文章中,我们已经对浮点系统进行了一些修改。只是它还不适合外汇。这是因为精度限制在小数点后四位,我们需要告诉系统我们将使用更多小数位的集合。我们需要解决这个问题,以避免将来出现其它问题。该修复在以下代码中完成:

C_Replay(const string szFileConfig)
    {
        m_ReplayCount = 0;
        m_dtPrevLoading = 0;
        m_Ticks.nTicks = 0;
        Print("************** Serviço Market Replay **************");
        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");
        CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
        m_IdReplay = (SetSymbolReplay(szFileConfig) ? 0 : -1);
    }

在此,我们将通知 MetaTrader 5,我们的浮点系统中需要更多的小数位。在这种情况下,我们要用到小数点后 8 位,这足以涵盖宽泛的条件。一个重要的细节:B3 可以很好地处理 4 位小数,但要操控外汇,我们需要 5 位小数。而使用 8 位,我们令系统自由。然而,情况并非总是如此。由于我还无法解释的细节,我们稍后将不得不更改它,但目前已经足够了。

一旦完成,我们将开始以某种方式让我们的生活更轻松。我们一开始将首先研究以下场景:图表上的预览柱线是 1-分钟柱线。至于跳价,它们是来自另一个文件中存在的真实跳价。因此,我们将回到最基本的系统,尽管我们很快就会打造一个更全面的系统。


运作基础知识

为了不强迫用户选择所要分析的市场类型,即回放数据源自的市场类型,我们将利用这样一个事实,即在某些情况下我们没有最后价格或交易量的数值,而在其它情况下我们能得到它。若启用系统为我们检查这一点,我们就不得不在代码中添加一些东西。

首先,我们添加以下内容:

class C_FileTicks
{
    protected:
        enum ePlotType {PRICE_EXCHANGE, PRICE_FOREX};
        struct st00
        {
            MqlTick   Info[];
            MqlRates  Rate[];
            int       nTicks,
                      nRate;
            bool      bTickReal;
            ePlotType ModePlot;
        }m_Ticks;

//... The rest of the class code....

该枚举将帮助我们避免在某些区域出现混淆。本质上,我们将其缩窄到两种市场类型。您很快就会明白其中的原因。为了避免不必要的函数调用,我们往系统里加入一个新变量。现在事情开始成形,但当我们使用特定的显示方法时,我们需要系统能够进行识别。我不想让这样的问题令用户的生活复杂化,故我们在下面的代码里做一个小小的改动:

inline bool ReadAllsTicks(const bool ToReplay)
        {
#define def_LIMIT (INT_MAX - 2)
#define def_Ticks m_Ticks.Info[m_Ticks.nTicks]

                string   szInfo;
                MqlRates rate;

                Print("Loading ticks for replay. Please wait...");
                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                m_Ticks.ModePlot = PRICE_FOREX;
                while ((!FileIsEnding(m_File)) && (m_Ticks.nTicks < def_LIMIT) && (!_StopFlag))
                {
                        ArrayResize(m_Ticks.Info, m_Ticks.nTicks + 1, def_MaxSizeArray);
                        szInfo = FileReadString(m_File) + " " + FileReadString(m_File);
                        def_Ticks.time = StringToTime(StringSubstr(szInfo, 0, 19));
                        def_Ticks.time_msc = (def_Ticks.time * 1000) + (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                        def_Ticks.bid = StringToDouble(FileReadString(m_File));
                        def_Ticks.ask = StringToDouble(FileReadString(m_File));
                        def_Ticks.last = StringToDouble(FileReadString(m_File));
                        def_Ticks.volume_real = StringToDouble(FileReadString(m_File));
                        def_Ticks.flags = (uchar)StringToInteger(FileReadString(m_File));
                        m_Ticks.ModePlot = (def_Ticks.volume_real > 0.0 ? PRICE_EXCHANGE : m_Ticks.ModePlot);
                        if (def_Ticks.volume_real > 0.0)
                        {
                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                                m_Ticks.nRate += (BuiderBar1Min(rate, def_Ticks) ? 1 : 0);
                                m_Ticks.Rate[m_Ticks.nRate] = rate;
                        }
                        m_Ticks.nTicks++;
                }
                FileClose(m_File);
                if (m_Ticks.nTicks == def_LIMIT)
                {
                        Print("Too much data in the tick file.\nCannot continue...");
                        return false;
                }
                return (!_StopFlag);
#undef def_Ticks
#undef def_LIMIT
        }

我们首先指定显示类型是外汇模型。不过,如果在读取跳价文件时发现包含交易量,则此模型将更改为证券类型。重点是要明白,这是在没有任何用户干预的情况下发生的。但这里浮现一个重点:该系统仅在回放启动期间执行读取时针对该情况起作用。在模拟的情况下,情况则有所不同。我们现在暂时不与模拟打交道。

出于此原因,直到我们创建模拟代码之前,不要仅使用柱线文件。必须使用跳价文件,无论是真实的还是模拟的。有多种方式可以创建模拟的跳价文件,但我不会详解,因为它超出了本文的范围。然而,我们不能让用户完全蒙在鼓里。即便系统可以分析数据,我们也可以向用户展示正在使用的显示类型。因此,通过打开“品种”窗口,我们可以检查显示形式。如图例 01 和图例 02 所示。

为了令这一点成为可能,我们需要在代码中添加更多内容。这些包括如下所示的行:

datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
    {
        int      MemNRates,
                 MemNTicks;
        datetime dtRet = TimeCurrent();
        MqlRates RatesLocal[];

        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if (!Open(szFileNameCSV)) return 0;
        if (!ReadAllsTicks(ToReplay)) return 0;
        if (!ToReplay)
        {
            ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
            ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
            CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
            dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
            m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
            m_Ticks.nTicks = MemNTicks;
            ArrayFree(RatesLocal);
        }else
        {
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
        }
        m_Ticks.bTickReal = true;

        return dtRet;
    };

通过添加这些高亮显示的行,我们就能获得有关该品种更充分的信息。但我希望您注意我将要解释的内容,因为如果您不理解这里提出的任何概念,您会认为系统在跟您耍花招。图例 05 显示了上述代码的作用。尝试将其用于其它品种,会令事情更清晰。第一点与该特殊行有关。它将显示我们可以或将用于品种的计算类型。是的,计算品种的方式有很多,但由于此处的思路是让它尽可能简单,且它仍可工作,故我们蒸发杂项,仅剩这两种计算类型。

如果您想获取更多详细信息,或实现其它计算类型,可以参考文档,并查看 SYMBOL_TRADE_CALC_MODE。在那里,您能找到每种计算模式的详细说明。在此,我们仅以最简单的方式工作。有一件事也许会让您发疯,那就是高亮显示的第二行。在这一行中,我们简单地指示显示模式类型。基本上,只有这两种类型。这里的问题不在于这一行本身,而在于配置文件,或者更准确地说,在于当前开发阶段如何读取配置文件。

目前,系统是按这种方式编写,如果我们先读取柱线文件,然后再读取跳价文件,我们就会遇到问题。这并不是因为我们做错了什么,与其对比,我们正遵循正确的逻辑。不过,事实上这行是在柱线文件加载到图表中后执行的,则会导致所有图表中存在的内容被删除。此问题是由于数据未经缓冲而直接进入图表引起的,故我们必须对此进行调查。不过,本文稍后将讨论解决方案。

就我个人观点,如果该系统仅供我个人使用,那么一切都会以不同的方式完成。我们只需生成某种类型的警报,如此可在柱线文件之前读取跳价文件。但由于该系统的常用人群都没有编程知识,于我看来,这样解决该问题似乎挺合适的。这甚至很好,因为您将学到如何做一个非常有趣,且非常实用的技巧。

图例 05

图例 05:自动跳价读数识别的显示

现在系统可以识别一些东西,我们需要它以某种方式适配我们的显示系统。

class C_Replay : private C_ConfigService
{
    private :
        long         m_IdReplay;
        struct st01
        {
            MqlRates Rate[1];
            datetime memDT;
            int      delay;
        }m_MountBar;
        struct st02
        {
            bool     bInit;
        }m_Infos;

// ... The rest of the class code....

为了配置所有内容并保存设置,我们将首先添加此变量。它将显示系统是否已完全初始化。但请注意我们将如何使用它。首先,我们用相应的值初始化它。它是在代码的以下片段中做到的:

C_Replay(const string szFileConfig)
{
    m_ReplayCount = 0;
    m_dtPrevLoading = 0;
    m_Ticks.nTicks = 0;
    m_Infos.bInit = false;
    Print("************** Serviço Market Replay **************");
    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");
    CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
    m_IdReplay = (SetSymbolReplay(szFileConfig) ? 0 : -1);
}

为什么我们将变量初始化为 false?原因是系统还没有初始化,然一旦我们将图表加载到屏幕上,它就会被初始化。您可能会认为我们会在初始化图表的函数中指定这一点。对否?否。我们不能在初始化图表的函数中执行此操作。我们必须等待这个初始化函数完成,且在调用下一个函数之时,我们就可以指示系统已经初始化。但为什么要用到这个变量呢?系统真的不知道它是否已经初始化了吗?为什么我们必须使用变量?系统知道它已初始化,但出于另一个原因,我们需要这个变量。为了清楚起见,我们来看看改变其状态的函数。

bool LoopEventOnTime(const bool bViewBuider, const bool bViewMetrics)
        {
                u_Interprocess Info;
                int iPos, iTest;

                if (!m_Infos.bInit)
                {
                        ChartSetInteger(m_IdReplay, CHART_SHOW_ASK_LINE, m_Ticks.ModePlot == PRICE_FOREX);
                        ChartSetInteger(m_IdReplay, CHART_SHOW_BID_LINE, m_Ticks.ModePlot == PRICE_FOREX);
                        ChartSetInteger(m_IdReplay, CHART_SHOW_LAST_LINE, m_Ticks.ModePlot == PRICE_EXCHANGE);
                        m_Infos.bInit = true;
                }
                iTest = 0;
                while ((iTest == 0) && (!_StopFlag))
                {
                        iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
                        iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
                        iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
                        if (iTest == 0) Sleep(100);
                }
                if ((iTest < 0) || (_StopFlag)) return false;
                AdjustPositionToReplay(bViewBuider);
                m_MountBar.delay = 0;
                while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
                {
                        CreateBarInReplay(bViewMetrics, true);
                        iPos = (int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - 1].time_msc : 0);
                        m_MountBar.delay += (iPos < 0 ? iPos + 1000 : iPos);
                        if (m_MountBar.delay > 400)
                        {
                                if (ChartSymbol(m_IdReplay) == "") break;
                                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                if (!Info.s_Infos.isPlay) return true;
                                Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                Sleep(m_MountBar.delay - 20);
                                m_MountBar.delay = 0;
                        }
                }                               
                return (m_ReplayCount == m_Ticks.nTicks);
        }

上面讲解的这个函数令我们有些头疼。这就是为什么我们需要一个变量来指示系统是否已初始化。请注意:首次运行此函数时,该变量仍指示系统尚未完全初始化。此时,其初始化将完成。于此,我们要确保屏幕上显示正确的价格线。如果系统将显示模式定义为外汇,则应显示出价和要价线,并隐藏最后交易价格线。如果显示模式为证券,则情况恰恰相反。在这种情况下,出价和要价线将被隐藏,并显示最后的交易价格线。

如果不是因为某些用户更喜欢不同的设置,一切都会非常好。即使他们在证券显示样式中工作,他们也喜欢显示出价或要价线,在某些情况下还会两者都显示。因此,如果用户在根据自己的喜好配置系统后暂停系统,然后再重启系统,系统会忽略用户的设置,并恢复到其内部设置。不过,通过指定系统已经初始化(并为此使用一个变量),它就不会返回到内部设置,而是保持用户刚刚配置的方式。

但随之浮现的问题是:为什么不在 ViewReplay 函数中进行设置?原因是图表实际上并未收到此类线条设置。我们还必须解决其它不太愉快的问题。我们需要额外的变量来帮助我们。简单的编程并不能解决所有问题。


显示柱线

我们终于到了可以在图表上显示柱线的环节。不过,如果在此阶段尝试执行该操作,则会遇到数组范围错误。因此,在图表上实际显示柱线之前,我们需要对系统进行一些修正。

第一处修复是在以下函数当中:

void AdjustPositionToReplay(const bool bViewBuider)
        {
                u_Interprocess Info;
                MqlRates       Rate[def_BarsDiary];
                int            iPos, nCount;

                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                if ((m_ReplayCount == 0) && (m_Ticks.ModePlot == PRICE_EXCHANGE))
                for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++);
                if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return;
                iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
                Rate[0].time = macroRemoveSec(m_Ticks.Info[iPos].time);
                if (iPos < m_ReplayCount)
                {
                        CustomRatesDelete(def_SymbolReplay, Rate[0].time, LONG_MAX);
                        CustomTicksDelete(def_SymbolReplay, m_Ticks.Info[iPos].time_msc, LONG_MAX);
                        if ((m_dtPrevLoading == 0) && (iPos == 0)) FirstBarNULL(); else
                        {
                                for(Rate[0].time -= 60; (m_ReplayCount > 0) && (Rate[0].time <= macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)); m_ReplayCount--);
                                m_ReplayCount++;
                        }
                }else if (iPos > m_ReplayCount)
                {
                        if (bViewBuider)
                        {
                                Info.s_Infos.isWait = true;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                        }else
                        {
                                for(; Rate[0].time > (m_Ticks.Info[m_ReplayCount].time); m_ReplayCount++);
                                for (nCount = 0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++);
                                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount);
                        }
                }
                for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay(false, false);
                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                Info.s_Infos.isWait = false;
                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        }

这项检查允许系统判定是否要跳过前几个跳价。问题是,如果我们不检查是否正在使用证券显示,那么检查中包含的循环将失败。这将导致范围错误。不过,通过添加第二次检查,我们就能度过此阶段。如果显示模式与外汇所用的类型匹配,则循环不会执行。如此,我们为下一阶段做准备。

在下一步中,我们实际上是把跳价插入到图表当中。于此,我们唯一需要担心的是告诉系统柱线收盘价是多少;其余部分则由柱线模拟函数处理。在这种情况下,证券和外汇模式显示两者都是相同的。其实现代码如下所示:

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 (bNew = (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;
        }
        def_Rate.close = (m_Ticks.ModePlot == PRICE_EXCHANGE ? (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close) :
                                                               (m_Ticks.Info[m_ReplayCount].bid > 0.0 ? m_Ticks.Info[m_ReplayCount].bid : 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
    }

这行正是这样做的。它将根据所使用的显示类型生成柱线收盘价。否则,函数就与以前相同。以这种方式,我们就能够涵盖外汇映射系统,并使用跳价文件中显示的数据回放。我们还没有能力进行模拟。

您可能认为系统已经完成,但事实并非如此。我们仍然有两个问题,在我们思考外汇模拟之前,它们相关性很强。

第一个问题是我们需要回放/模拟配置文件具有创建逻辑。在许多情况下,这将迫使用户在没有实际需要的情况下适配系统。除此之外,我们还有另一个问题。定时器系统。第二个问题的原因在于,我们可能正在处理一个品种或交易时间,该品种可能在数小时内保持非活动状态,或者因为它被拍卖或暂停,或者由于其它原因。但这并不重要,我们还需要修复这个计时器问题。

既然第二个问题比较紧迫,那就从它开始吧。


修复计时器

系统和计时器最大的问题是系统无法处理某些品种中有时会出现的条件。这种条件也许是极低的流动性、业务暂停、拍卖、或其它原因。若出于某种原因,跳价文件告诉计时器该品种应该休眠 15 分钟,则系统将在这段时间内完全锁定。

在真实市场中,这是以一种特殊的方式解决的。典型情况下,如果一个品种不交易,平台会通知我们,但即使平台没有向我们提供这些信息,我们仍然会收到来自市场的通知。拥有更多经验的交易者,查看一个品种,就会注意到发生了一些事情,在此期间不要做任何事情。不过,如果是用市场回放,这种状况就会出问题。我们必须允许用户关闭或尝试修改回放或模拟的运行位置。

这种类型的解决方案以前曾经运用过,实际上,可以用一个控制指示器。然而,我们没遇到先例,迫使我们采取如此严厉的措施,甚至于一个品种被拍卖的状况,流动性非常低,甚至由于相关事件而被暂停。所有这些都产生了所谓的流动性风险,但在回放/模拟系统中,我们可以很容易地避免这种风险,并继续我们的研究。不过,为了能有效做到这一点,我们需要改变计时器的工作方式。

接下来,我们就有一个新的显示系统循环。我知道代码乍一看也许令人困惑。

bool LoopEventOnTime(const bool bViewBuider, const bool bViewMetrics)
    {
        u_Interprocess Info;
        int iPos, iTest;

        iTest = 0;
        while ((iTest == 0) && (!_StopFlag))
        {
            iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
            iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
            iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
            if (iTest == 0) Sleep(100);
        }
        if ((iTest < 0) || (_StopFlag)) return false;
        AdjustPositionToReplay(bViewBuider);
        iPos = 0;
        while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
        {
            iPos = (int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - 1].time_msc : 0);
            m_MountBar.delay += (iPos < 0 ? iPos + 1000 : iPos);
            iPos += (int)(m_ReplayCount < (m_Ticks.nTicks - 1) ? m_Ticks.Info[m_ReplayCount + 1].time_msc - m_Ticks.Info[m_ReplayCount].time_msc : 0);
            CreateBarInReplay(bViewMetrics, true);
            if (m_MountBar.delay > 400)
            while ((iPos > 200) && (!_StopFlag))
            {
                if (ChartSymbol(m_IdReplay) == "") break;
                if (ChartSymbol(m_IdReplay) == "") return false;
                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                if (!Info.s_Infos.isPlay) return true;
                Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                Sleep(195);
                iPos -= 200;
                Sleep(m_MountBar.delay - 20);
                m_MountBar.delay = 0;
            }
        }                               
        return (m_ReplayCount == m_Ticks.nTicks);
    }

所有删除的部分均已由其它代码替换。因此,我们可以解决与柱线显示相关的第一个问题。以前我们使用反向计算,但现在我们将使用正向计算。这样可以防止我们在空闲模式下图表上发生一些奇怪的事情。请注意,当我们启动系统时,它在显示跳价之前总会等待一段时间。以前,它的做法完全相反(我的错)。由于时间现在可以很长,长达若干个小时,因此我们有另一种方法来控制计时器。为了更好地理解,您应该知道以前系统一直处于待机模式,直到时间完全到期。如果我们尝试进行任何变更,例如关闭图表、或尝试更改执行点,系统不会如预期响应。发生这种情况是因为在之前附带的那些文件中,不存在所用资产长时间保持“非交易”状态集合的风险。但当我开始撰写这篇文章时,系统显示出这个错误。因此,已经进行了更正。

计时器现在将运行,直到时间段大于或等于 200 毫秒。您可以更改此数值,但要小心在其它位置也要相应更改。我们已经开始改善这种状况,但在我们完成之前,我们仍然需要再做一件事。如果关闭图表,系统将退出循环。现在返回调用程序。这确保了一切正常,至少理论上如此。这是因为用户可能会在空闲期间再次与系统交互。其余函数几乎没有变化,故一切都像以前一样继续工作。不过,如果在一段时间内您要求控制指标更改其位置,那么现在这将成为可能,而在此之前这是不可能的。这非常重要,因为有些资产可能会进入休眠状态,并停留很长时间。这在真实交易中是可以接受的,但在回放/模拟系统中则是不可接受的。


结束语

尽管遇到了种种挫折,但您现在可以开始尝试在系统中使用外汇数据了。我们从这个版本的回放/模拟系统开始。为了支持这一点,我在附件中提供了一些外汇数据。该系统在某些方面仍需改进。由于我还不打算深入细节(因为它可能需要对本文中显示的某些内容进行彻底的更改),因此我将在这里结束修改。

在下一篇文章中,我将定位一些其它未解决的问题。虽然这些问题不会妨碍您使用该系统,但如果您打算将其与外汇市场数据一起使用,您会注意到有些事情还没有正确呈现。我们需要稍微调整一下,但这将在下一篇文章中看到。

配置文件问题以及其它相关问题仍需解决。鉴于系统运行正常,允许您复现从外汇市场获得的数据,因此这些顽固问题的解决方案将推迟到下一篇文章。


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

附加的文件 |
Market_Replay_yvg20.zip (14386.04 KB)
DRAKON可视化编程语言 - 面向MQL开发人员和客户的通信工具 DRAKON可视化编程语言 - 面向MQL开发人员和客户的通信工具
DRAKON是一种可视化编程语言,旨在简化来自不同领域的专家(生物学家、物理学家、工程师…)与俄罗斯太空项目(例如,Buran可重复使用航天器项目)程序员之间的互动。在这篇文章中,我将讨论DRAKON如何使算法的创建变得容易和直观,即使你从未遇到过代码,以及客户在订购交易机器人时如何更容易解释他们的想法,以及程序员如何在复杂函数中减少错误。
MQL5 中的范畴论 (第 13 部分):数据库制程的日历事件 MQL5 中的范畴论 (第 13 部分):数据库制程的日历事件
本文在 MQL5 中遵循范畴论实现秩序,研究如何在 MQL5 中结合数据库制程进行分类。我们介绍了当辨别交易相关的文本(字符串)信息时,如何把数据库制程概念与范畴论相结合。日历事件是焦点。
用置信区间估计未来效能 用置信区间估计未来效能
在这篇文章中,我们深入研究自举法技术的应用,作为评估自动化策略未来效能的一种手段。
配对交易 配对交易
在这篇文章中,我们将探讨配对交易(pair trading),即它的原理是什么,以及它的实际应用是否有前景。我们还将尝试创建一个配对交易策略。