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

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

MetaTrader 5测试者 | 2 四月 2024, 09:56
443 0
Daniel Jose
Daniel Jose

概述

在上一篇文章“开发回放系统 — 市场模拟(第 21 部分):外汇(II)”中,我们主要关注在解决系统问题,尤其是与回放/模拟配置文件相关的问题。不过,鉴于这里已经有很多系统研究和后续文章的信息,可供那些想要学习如何自行创建程序的人士所用,故我们决定在设置系统已经处于正工作状态时结束文章,如此我们就可在相当长的时间内舒服地工作。

但如果您已经测试了附件,您可能已经注意到,虽然回放/模拟系统能在股票市场中非常稳定地工作,但在外汇市场却不敢这样说。并且在此,我指的不仅仅是外汇本身,而是任何遵循与外汇相同价格表述概念的资产,即采用出价(BID)作为基准价。

虽然这是关于这个主题的第三篇文章,但我必须为那些还不了解股票市场和外汇市场之间区别的人解释一下:最大的区别在于,在外汇中没有、或者更确切地说,我们得不到交易过程中有关一些实际发生关键处的信息。虽然看似这只适用于外汇市场,但我并未暗示排他性。这是因为在外汇市场中,我们有自己的方法去获取某些信息,且交易模式与股票市场模式完全不同。我认为以这种方式进行区分更容易。在这些文章中,与外汇相关的任何内容都应理解为适用于通过出价(BID)表述的任何类型的市场,这与使用最后成交价的股票市场中所发生的情况大相径。

因此,按股票市场相同的方式覆盖外汇市场(已经被市场回放系统涵盖),我们就能够回放或模拟任何类型的市场。系统中仍然给我们带来问题的一件事是,如果我们禁用显示由回放或模拟器生成的柱线,则图形看起来不正确。我们过去在开发一个涵盖股票市场的系统时遇到过这个问题。但由于外汇市场的显示方式不同,基准价格是出价,因此当前的系统无法正确处理这个问题。如果在关闭柱线显示时您尝试将所研究的位置移动到另一个点,则所有指标都将不正确,因为旧位置和新位置之间没有柱线图。因此,我们在本文中将从解决此问题开始。


校正快速定位系统

为了解决这个问题,我们只得针对代码进行一些修改,不是因为它不正确,而是因为它不能操控出价绘图。更准确地说,问题在于读取跳价并将其转换为 1-分钟柱线(即使在 C_FileTicks 类中)以供后续快速定位过程所用这有些不切实际。这是因为我们无法根据出价来表示价格值。为了搞明白为何会这样,我们来看一下负责执行此转换的代码。这段代码如下所见:

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
    }

请注意,调用创建柱线的函数,必须有一个交易量。仅当最后成交价发生变化时,才会出现交易量。当采用出价时,则交易量始终为零,这样子例程就无法调用。我们需要做的第一件事是从这里删除这段代码,因为我们实际上还不知道在读取跳价时我们是取出价或最后成交价进行绘图。因此,上述函数将修改如下:

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

        string   szInfo;
            
        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);
            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
    }

我为什么要这样做?原因是只当读取整个文件后,我们才会知道是取出价还是最后成交价进行绘图。也就是说,这行保证了这一点。不过,它仅在读取文件后才有效。当我们调用跳价转换为柱线时会发生什么,在显示阶段将用到什么?目前,我们不讨论这个问题。请参阅以下函数:

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;
    };

在上述函数中,我们无法在任何位置调用跳价转换函数。这是因为在某些时候,我们需要将跳价转换为柱线,且我们将使用真实的跳价文件,就好像它是以前的 1-分钟柱线文件一样。这可以从上面代码存在的 CustomRatesUpdate 函数中看出。出于此原因,我们必须在调用 CustomRatesUpdate 之前调用转换函数。然而,如果您查看一下转换函数,您会发现它不适合在上面的代码中使用。函数源码如下所示:

inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick)
    {
        if (rate.time != macroRemoveSec(tick.time))
        {
            rate.real_volume = 0;
            rate.tick_volume = 0;
            rate.time = macroRemoveSec(tick.time);
            rate.open = rate.low = rate.high = rate.close = tick.last;
        
            return true;
        }
        rate.close = tick.last;
        rate.high = (rate.close > rate.high ? rate.close : rate.high);
        rate.low = (rate.close < rate.low ? rate.close : rate.low);
        rate.real_volume += (long) tick.volume_real;
        rate.tick_volume += (tick.last > 0 ? 1 : 0);

        return false;
    }

在我们需要的地方调用该函数很不方便。因此,我们只得另创一个函数来执行转换。除此之外,我们还有另一个问题。我们怎么知道从哪里开始转换?记住,调用可能会在不同的时间发生,我们每次都可能有不同的需求。在其中一次调用中,我们可以下载用于回放的跳价。在另一次调用中,我们加载的跳价必须在不久后丢弃,因为它们只是用作生成以前的柱线。如您所见,这需要精心考虑,以免陷入死胡同。

如果您分析这些需要进行的修改,并完全按计划实现它们,所有这些都会变得更加简单和容易。故此,我们将从修改调用过程开始,然后是转换函数的工作。新的调用过程如下所示:

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;
        BuiderBar1Min(MemNTicks);
        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;
    };

注意我们如何解决问题的第一部分。放置这行,我们确保在可能调用 CustomRatesUpdate 之前将跳价转换为 1-分钟柱线。同时,我们告诉转换函数应从何处开始转换过程。我们把转换函数调用放在此处而不是放在读取函数当中,是为了避免往读取函数里添加不必要的变量。在此,我们就可访问检测工作范围所需的变量。现在我们能够转入实现跳价转换为 1-分钟柱线的函数。我们必须以一种既适用于外汇类型市场,又适用于证券交易所类型市场的方式来做到这一点。看起来很复杂,不是吗?同样,如果您还没有计划好要做什么,您可能会陷入一个编码漩涡,这将导致您放弃实现正确代码的尝试。

我不打算在此给您一个解决方案:我希望您学会按这种方式去思考,如此您就可以找到一个适合您的解决方案。现在,我们来思考以下几点:最初的转换过程已经允许我们将最后成交价转换为 1-分钟柱线。我们需要在该过程中加上一个循环,如此它就能从文件中读取的所有跳价。这个循环的起点由调用程序传递给我们,终点则是最后一个读取的跳价。到目前为止,一切进展顺利。但我们还需要确保,如果系统检测到我们是基于出价值绘图,则该值将替换最初使用的最后成交价。以这种方式,我们就能毫不费力地将出价值转换为 1-分钟柱线。挺有趣,是不是?这个思路的实现如下图所示:

inline void BuiderBar1Min(const int iFirst)
    {
        MqlRates rate;
        double   dClose = 0;
        
        rate.time = 0;
        for (int c0 = iFirst; c0 < m_Ticks.nTicks; c0++)
        {
            switch (m_Ticks.ModePlot)
            {
                case PRICE_EXCHANGE:
                    if (m_Ticks.Info[c0].last == 0.0) continue;
                    dClose = m_Ticks.Info[c0].last;
                    break;
                case PRICE_FOREX:
                    dClose = (m_Ticks.Info[c0].bid > 0.0 ? m_Ticks.Info[c0].bid : dClose);
                    if (dClose == 0.0) continue;
                    break;
            }
            if (rate.time != macroRemoveSec(m_Ticks.Info[c0].time))
            {
                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                rate.time = macroRemoveSec(m_Ticks.Info[c0].time);
                rate.real_volume = 0;
                rate.tick_volume = 0;
                rate.open = rate.low = rate.high = rate.close = dClose;
            }else
            {
                rate.close = dClose;
                rate.high = (rate.close > rate.high ? rate.close : rate.high);
                rate.low = (rate.close < rate.low ? rate.close : rate.low);
                rate.real_volume += (long) m_Ticks.Info[c0].volume_real;
                rate.tick_volume++;
            }
            m_Ticks.Rate[(m_Ticks.nRate += (rate.tick_volume == 0 ? 1 : 0))] = rate;
        }
    }

原始函数以绿色高亮显示,帮助您识别它所在的位置。这些代码是该函数的新部分。这部分整体负责将最后成交价转换为出价,反之亦然,如此收盘价适合创建 1-分钟柱线。我早前提到的循环正处于这个阶段。虽然这个函数解决了我们的大部分问题,但如果我们采用出价类型绘图,它并不能解决跳价交易量问题。为了解决这个问题,我们只得稍微修改前面的函数,故最终的代码看起来像这样:

inline void BuiderBar1Min(const int iFirst)
    {
        MqlRates rate;
        double  dClose = 0;
        bool    bNew;
        
        rate.time = 0;
        for (int c0 = iFirst; c0 < m_Ticks.nTicks; c0++)
        {
            switch (m_Ticks.ModePlot)
            {
                case PRICE_EXCHANGE:
                    if (m_Ticks.Info[c0].last == 0.0) continue;
                    dClose = m_Ticks.Info[c0].last;
                    break;
                case PRICE_FOREX:
                    dClose = (m_Ticks.Info[c0].bid > 0.0 ? m_Ticks.Info[c0].bid : dClose);
                    if ((dClose == 0.0) || (m_Ticks.Info[c0].bid == 0.0)) continue;
                    break;
            }
            if (bNew = (rate.time != macroRemoveSec(m_Ticks.Info[c0].time)))
            {
                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                rate.time = macroRemoveSec(m_Ticks.Info[c0].time);
                rate.real_volume = 0;
                rate.tick_volume = (m_Ticks.ModePlot == PRICE_FOREX ? 1 : 0);
                rate.open = rate.low = rate.high = rate.close = dClose;
            }else
            {
                rate.close = dClose;
                rate.high = (rate.close > rate.high ? rate.close : rate.high);
                rate.low = (rate.close < rate.low ? rate.close : rate.low);
                rate.real_volume += (long) m_Ticks.Info[c0].volume_real;
                rate.tick_volume++;
            }
            m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
        }
    }

在这段代码中,实际解决跳价交易量问题,我们必须添加一个新变量。当我们评估是否添加新柱线时,其值是在这个阶段确定的。该变量仅用在此处,我们据其可知是否要向系统添加新柱线。

请注意以下事项:当我们采用最后成交价绘图模式时,为了令跳价交易量正确,我们必须从头开始计数器。但当我们采用出价绘图时,我们必须从 1 开始计数。否则,我们将得到不正确的跳价数据。

原因在于细节。但如果您未采取正确的预防措施,它们可能会伤害您。


校正跳价交易量

您也许认为系统中不会有更多错误。然而,仍然存在一些需要解决的缺点,其中之一就是交易量问题。在上一个主题中,我们更正了系统在正分析的位置受快速变化事件影响时报告的跳价交易量。但如果您在位置没有快速变化的情况下运行回放或模拟,则交易量信息就不正确了。

这并不是说代码是错误的,恰恰相反。如果您正用一个系统回放数据或模拟资产,是采用“最后成交价”值绘图,则报告的跳价交易量就是正确的。但如果您采用出价,就像在外汇中发生的那样,则该交易量将不正确。我们现在需要解决这个问题,以便交易量信息正确。为了明白问题所在,我们来看一下负责执行此计算的代码:

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
    }

在基于出价表述的资产情况下,此计算将不起作用,因为这种交易量类型在外汇中根本不存在。不过,此处的计算对于基于出价的显示系统会生成不正确的值。这是因为在这些情况下,跳价不包含交易量相关的信息。因此,跳价成交量值会始终为零。

然而,在创建 1-分钟柱线的阶段生成跳价交易量的计算中,该计算会给出正确的值。因此,我们必须修正上述代码。它看起来像这样:

inline void CreateBarInReplay(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);            
            m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
            if (m_Ticks.ModePlot == PRICE_FOREX) CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate, (def_Rate.time < m_MountBar.memDT ? 1 : 0));
            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.ModePlot == PRICE_FOREX) && (m_Ticks.Info[m_ReplayCount].bid > 0.0) ? 1 : (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
    }

不要问我为什么,但出于我个人不知道的一些奇怪原因,我们必须在这里添加这一行。如果您不添加它,则跳价交易量中所示的值将不正确。注意函数中有一个条件。这就避免了快速定位系统使用当中出现的问题,并防止在系统图表上出现不合时宜的奇怪柱线。虽然这是一个非常奇怪的原因,但其它一切都按预期进行。这将是一种新的计算,其中我们将按相同的方式计数跳价 — 无论是采用基于出价的资产,亦或采用基于最后成交价的金融产品。

您也许已经注意到,该计算非常简单。但有趣的是,我们必须再次在自定义资产中发送兑换值。柱线收盘后。我还不明白其中的原因。一切都太奇怪了,只有在采用出价类型时才需要这样发送,这非常有趣。

在上一个函数中,您可能还注意到了一件事。现在,衡量系统已不复存在。从某种意义上说,当将跳价添加到市场观察窗口时,我曾考虑删除这个系统。这是因为我们可以准确地估算创建每根柱线所需的时间。故此,删除了衡量代码。


下一次测试的设置阶段

到目前为止,随着所有修改的实现,我们可以转入真正的任务:创建一种外汇市场跳价建模的方式,仅基于 1-分钟柱线文件中存在的内容。相信我,挑战将是相当艰巨的,但同时也非常有趣和令人兴奋。为了方便起见,我们将 C_FileTicks 类拆分为 2 个类,但这只是为了让问题更容易。这种拆分不是必需的,但由于我们将处理一些非常繁琐的任务,而且我不喜欢一个类的代码超过 1000 行,故我们把 C_FileTicks 拆分成两个类。

在本章节中,我们将从 C_FileTicks 类中删除跳价建模部分。C_Simulation 类负责将 1-分钟柱线转换为跳价,如此即可毫无问题地显示(和运行)。C_Simulation 类对回放系统是不可见的。对于回放服务,数据将始终来自真实的跳价文件。事实上,它们也许从模拟中出现。即使您尝试从 C_Replay 类访问 C_Simulation 类,也无法访问它。因此,一切都将按我们预期的方式工作,因为 C_Replay 类只能看到 C_FileTicks 类,该类加载文件中存在的实际跳价,如此 C_Replay 类即可在 MetaTrader 5 终端中显示它们。

新的 C_FileTicks 类声明现在如下所示:

#include "C_FileBars.mqh"
#include "C_Simulation.mqh"
//+------------------------------------------------------------------+
#define macroRemoveSec(A) (A - (A % 60))
//+------------------------------------------------------------------+
class C_FileTicks : private C_Simulation
{

// ... Internal class code 

};

结果就是,C_FileTicks 类将自 C_Simulation 类私密继承。以这种方式,我们肯定能达成上述内容。

但我们需要对 C_FileTicks 类的代码进行一些小的修改。这是因为我们继承了 C_Simulation 类。但我不想将声明为受保护的数据发送到 C_Simulation 类。这样做是为了确保 C_Simulation 类对系统的其余部分保持隐藏状态。不过,我们仍然需要允许该类完成的工作可供其它类使用,故我们需要添加以下代码:

bool BarsToTicks(const string szFileNameCSV)
    {
        C_FileBars  *pFileBars;
        int         iMem = m_Ticks.nTicks,
                    iRet;
        MqlRates    rate[1];
        MqlTick     local[];
        
        pFileBars = new C_FileBars(szFileNameCSV);
        ArrayResize(local, def_MaxSizeArray);
        Print("Converting bars to ticks. Please wait...");
        while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
        {
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
            iRet = Simulation(rate[0], local);
            for (int c0 = 0; c0 <= iRet; c0++)
            {
                ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
            }
        }
        ArrayFree(local);
        delete pFileBars;
        m_Ticks.bTickReal = false;
        
        return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
    }

高亮显示的行是代码的一部分,执行创建模拟的过程。为了一切能按部就班,我们现在需要一个函数来生成模拟,并返回一个值。该值用于告诉 C_FileTicks 类应在数组中存储多少个跳价,以供 C_Replay 类以后使用。

现在,我们可以专注于 C_Simulation 类,并构建一个系统,其仅基于 1-分钟柱线文件中包含的数据执行任何级别的模拟。这将是本文下一个主题中要研究的主题。


C_Simulation 类

现在我们已经划分了所有内容,实际上将与任何类型的市场合作,我们需要确定一个小但非常重要的关键。我们要创建什么样的模拟?我们是否希望跳价在股票市场或外汇市场相同?这个问题似乎造成了混乱,并迫使我们使用两种模拟模式。思考以下情况:对您来说(毕竟,您正在进行系统编程),这个问题相对简单,纯粹是形式主义。但对于用户来说,这是一件令人困惑的事情,他们通常并不真正想知道数据库是针对外汇市场、还是股票市场进行模拟。他们只希望系统如常工作,即模拟数据。

但是,在处理 1-分钟柱线文件数据时,表面看,无法知道该特定文件中的数据是来自外汇市场、亦或股票市场。至少,乍一看。但如果您仔细查看这些文件,并比较其中的数据,您可能会注意到某种模式。幸好这种模式,可以清晰有效地定义应当使用的绘图类型:出价或最后成交价。为此,我们需要查看柱线文件的内容。

您作为程序员,应始终承担令程序适应特定绘图模型的任务。用户不应该学习 1-分钟柱线文件中存在的建模类型,因为应当由程序员为用户解决问题。如果您不明白我们在说什么,不要担心。我知道这似乎是一个奇怪的问题,但了解数据库如何解决这个特定问题后,我决定拆分模拟系统代码。这样就允许更好地实现。在转入 C_Simulation 类之前,我们了解如何区分出价和最后成交价类型绘图系统。

我想您明白系统如何知道我们是回放外汇还是股市的数据。如果您不明白,我建议您重新阅读以前的文章,直到您真正理解系统是如何做出这种区分的。但如果您已经明白了有关回放的部分,那么我们转入讨论重要问题:当唯一的信息来源是 1-分钟柱线文件时,系统如何知道它能否操控来自外汇或股市的数据?  

为了找出我们是否应该使用模拟器来创建跳价,并且信息是用于外汇市场、亦或股票市场,我们可以使用交易量!是的,确实是交易量!这正是它是如何做的。判定柱线文件是属于外汇(我们采用出价作为交易价格)、还是股票市场(我们采用最后成交价作为交易价格)的信息正是交易量。

如果您不明白这一点,我建议您查看下面的图像,您可以在其中看到含有 1-分钟柱线的文件部分。


外汇

图例 02 — 外汇资产文件


股市

图例 03 — 股市资产文件

我们没有是否正在用出价、或最后成交价作为实际交易价格指南的信息。在上图中,唯一的区别是交易量值。该值在图像中高亮显示,因此您可以看到差异所在。


本文的最终思索

现在我们再看一点。在文章“开发回放系统 — 市场模拟(第 11 部分):模拟器的诞生(I)”中,当我们启动模拟系统开发时,我们使用了 1-分钟柱线中存在的一些资源来模拟随机游走。正如文件中所报告的,这将是该 1-分钟柱线内可能的市场走势。然而,在构建此机制的过程中,包括在文章“开发回放系统 — 市场模拟(第 15 部分):模拟器的诞生(V)- 随机游走”中,我们构建了一个随机游走,其中我们研究了交易量为零或跳价交易量非常小的情况。但是,由于有些市场和资产拥有相当独特的含义,因此也有必要涵盖此类情况。如果您不这样做,并运行模拟,服务将在某个时候卡住或崩溃。记住,问题出现在模拟的情况下。在回放的情况下,只要一切都完全和谐,一切都会正常工作,因为没有必要操控异常状况,就像我们将来要做的事情一样,其中我们将不得不创建、或者更确切地说,模拟可能的市场走势。

至于现在,我会让您思考之前发生的事情。值得仔细考虑的部分是为外汇类型市场创建模拟器的部分,我们采用出价值进行绘图。这是因为若要明白系统,有数个关键点必须解释,而它们与迄今为止观察到的有很大不同。我们将不得不重新制定模拟计算的某些部分。下一篇文章再与您相见。



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

附加的文件 |
Market_Replay_7vc22.zip (14387.78 KB)
创建多交易品种、多周期指标 创建多交易品种、多周期指标
在本文中,我们将研究创建多交易品种、多周期指标的原则。我们还将了解如何从 EA 交易和其他指标中获取此类指标的数据。我们将探讨在 EA 交易和指标中使用多指标的主要功能,并将了解如何通过自定义指标缓冲区绘制它们。
MQL5 中的范畴论 (第 14 部分):线性序函子 MQL5 中的范畴论 (第 14 部分):线性序函子
本文是更广泛关于以 MQL5 实现范畴论系列的一部分,深入探讨了函子(Functors)。我们实验了如何将线性序映射到集合,这要归功于函子;通过研究两组数据,典型情况下会忽略其间的任何联系。
神经网络变得轻松(第五十一部分):行为-指引的扮演者-评论者(BAC) 神经网络变得轻松(第五十一部分):行为-指引的扮演者-评论者(BAC)
最后两篇文章研究了软性扮演者-评论者算法,该算法将熵正则化整合到奖励函数当中。这种方式在环境探索和模型开发之间取得平衡,但它仅适用于随机模型。本文提出了一种替代方式,能适用于随机模型和确定性模型两者。
了解如何在MQL5中处理日期和时间 了解如何在MQL5中处理日期和时间
这是一篇关于一个新的重要话题的新文章,这个话题是关于日期和时间的。作为交易工具的交易员或程序员,了解如何很好、有效地处理日期和时间这两个方面至关重要。因此,我将分享一些重要信息,关于我们如何处理日期和时间,以便顺利、简单地创建有效的交易工具。