English Русский 中文 Español Deutsch Português
preview
リプレイシステムの開発 - 市場シミュレーション(第22回):FOREX (III)

リプレイシステムの開発 - 市場シミュレーション(第22回):FOREX (III)

MetaTrader 5テスター | 30 3月 2024, 14:42
53 0
Daniel Jose
Daniel Jose

はじめに

前回の「リプレイシステムの開発 - 市場シミュレーション(第21回):FOREX (II)」稿では、主にシステムの問題、特にリプレイ/シミュレーションの構成ファイルに関する問題を解決することに集中しました。しかし、このシステムの記事を読んで勉強し、自分のプログラムの作り方を学ぼうとする方のために、ここにはすでに多くの情報があるので、セットアップシステムがすでにかなり長い間快適に作業できる状態になったところで、この記事を終えることにしました。

しかし、この添付ファイルをテストされた方は、リプレイ/シミュレーションシステムが株式市場では非常に安定して機能する一方で、FOREX市場では同じことが言えないことにお気づきかもしれません。ここでは、FOREXそのものを意味するのではなく、FOREXと同じ価格表示の概念に従った、つまり買値を基礎値とするあらゆる資産を意味します。

このトピックに関する記事は今回で3回目になりますが、株式市場とFOREX市場の違いをまだ理解していない方のために説明しなければなりません。大きな違いは、FOREXでは、取引の過程で実際に発生したいくつかのポイントに関する情報がないというか、与えられないということです。これはFOREX市場だけに当てはまるように見えるかもしれませんが、私はそれだけを意味しているのではありません。というのも、FOREX市場には独自の情報入手方法があり、取引モデルは株式市場モデルとはまったく異なるからです。こうやって区別した方がわかりやすいと思います。これらの記事の中でFOREXに関連するものはすべて、Last値が使用される株式市場で起こることとは対照的に、Bid値を通じて表現されるあらゆるタイプの市場に適用されるものとして理解されるべきです。

このように、株式市場(すでに市場リプレイシステムでカバーされている)と同じようにFOREX市場をカバーすることで、あらゆる種類の市場をリプレイまたはシミュレーションできるようになります。そして、システムで依然として発生している問題の1つは、リプレイまたはシミュレーターによって生成されたバーの作成の表示を無効にすると、グラフが正しく表示されないことです。過去に、株式市場をカバーするシステムを開発したときに、この問題に遭遇しました。しかし、FOREX市場では表示が異なり、基本価格がBidであるため、現在のシステムではこの問題に適切に対処できません。バー表示がオフになっているときに学習した位置を別のポイントに移動しようとすると、古い位置と新しい位置の間にバーがないため、すべての指標が不正確になります。そこで、この記事ではこの問題を解決することから始めることにします。


ラピッドポジショニングシステムの修正

この問題を解決するためには、コードを少し変更する必要があります。それは、コードが正しくないからではなく、Bidプロットでは動作しないからです。より正確に言えば、ティックを読み込んで1分足に変換し(たとえC_FileTicksクラスであっても)、その後の迅速なポジショニングプロセスで使用するのは現実的ではないということです。これは、Bidに基づく価格の値を表すことができないからです。その理由を理解するために、この変換をおこなうコードを見てみましょう。このコードを以下に示します。

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
    }

バーを作成する関数を呼び出すには、取引された出来高がなければならないことにご注意ください。この出来高は、最後の価格値が変化したときにのみ発生します。Bid価格を使用する場合、この出来高は常にゼロになります。つまり、サブルーチンは呼び出されません。最初におこなう必要があるのは、このコードをここから削除することです。なぜなら、ティックを読み取るときにBidとLastのどちらでプロットするのかがわからないからです。そこで、上記の関数を以下のように変更します。

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

必要なところでこの関数が使えないのは不便です。したがって、変換を実行するために別の関数を作成する必要があります。しかし、これ以外にもう1つ問題があります。どこから変換を開始すればいいのか、どうやって知るのでしょうか。呼び出しのタイミングはさまざまで、その都度ニーズが異なる可能性があることを覚えておいてください。ある呼び出しでは、リプレイで使用するためにティックをダウンロードすることができました。別の呼び出しでは、ティックを読み込むことができましたが、そのティックは前のバーとして使用されるため、すぐに破棄しなければなりません。おわかりのように、行き詰まらないように慎重に考える必要があります。

必要な変更を分析し、計画通りに実行に移せば、こうしたことはずっとシンプルで簡単になります。そこで、まず呼び出しプロシージャを変更し、それから変換関数に取り組むことにします。新しい呼び出し手順を以下に示します。

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分足に変換されるようにします。同時に、変換機能に対して、どこから変換処理を始めるべきかを指示します。不要な変数をread関数に追加するのを避けるため、変換関数の呼び出しをread関数内ではなく、ここに配置します。ここでは、作業範囲を決定するために必要な変数にアクセスできます。ここで、ティックを1分足に変換する関数の実装に移ることができます。FOREX市場と為替市場の両方に対応できるようにしなければなりません。複雑に見えるでしょうか。繰り返しになりますが、何をするのかを計画しなければ、コーディングサイクルにはまり込み、正しいコードを実装することをあきらめてしまうことになります。

ここで解決策を示したくはありません。自分に合った解決策を見つけられるような考え方を身につけてほしいのです。では、次のことを考えてみましょう。当初の変換手順では、すでに最終価格を1分足に変換することができました。このプロシージャにループを追加して、ファイルから読み込んだすべてのティックりを読み込むようにする必要があります。このループの始点は呼び出し元のプログラムから渡され、終点は最後に読み込まれたティックとなります。今のところすべて順調です。しかし、システムがBid値に基づいてプロットを使用していることを検出した場合、この値が元々使用されていたLast価格に置き換わるようにする必要もあります。こうすることで、手間なくBid値を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分足バーを作成するのに適しているように、LastをBidに、またはその逆に交換することを全面的に担当します。先ほどのループはこの段階にあります。この関数はほとんどの問題を解決してくれますが、Bidタイプのプロットを使用する場合、ティック出来高の問題は解決しません。この問題を解決するには、前の関数を少し修正して、最終的なコードが次のようになるようにしなければなりません。

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

ティック出来高の問題を実際に解決するこのコードでは、新しい変数を追加しなければなりませんでした。その値は、新しいバーを追加するかどうかを評価するこの段階で決定されます。この変数は、システムに新しいバーを追加するかどうかを知らせるためだけに使用されます。

以下の点にご注意ください。Lastプロットモードを使用する場合、ティック出来高を正しくするためには、カウンタをゼロから開始しなければなりません。しかし、Bidによるプロットを使用する場合は、値を1から始めなければなりません。そうでなければ、ティック出来高で誤ったデータを取得することになります。

その理由は細部にあります。しかし、適切な予防措置を講じなければ、危害を加える可能性があります。


ティック出来高の修正

もうシステムにエラーはないと思うかもしれません。しかし、まだ解決すべき欠点があり、その1つが出来高の問題です。前回のトピックでは、分析中のポジションが急激に変化した場合にシステムが報告するティックの量を修正しました。しかし、出来高を素早く変更せずにリプレイやシミュレーションを実行すると、出来高情報が不正確になります。

コードが間違っているのではなく、その正反対です。データをリプレイするシステムを使用している場合、またはプロットにLast値を使用する資産をシミュレーションしている場合、報告されたティック出来高は正しくなります。しかし、FOREXで起こるようにBid価格を使用すると、この出来高は不正確になります。出来高情報が正しくなるように、この問題を解決する必要があります。何が問題なのかを理解するために、この計算をおこなうコードを見てみましょう。

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
    }

Bidベースで表示される資産の場合、このような出来高はFOREXには存在しないため、この計算は何の効果もありません。しかし、ここでの計算は、Bidベースの表示システムにとっては正しくない値を生成することになります。これは、このような場合、ティックに取引量に関する情報が含まれていないためです。したがって、ティック出来高の値は常にゼロとなります。

しかし、動きが速い場合に使用される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
    }

理由は聞かないでください。しかし、個人的にはわからない奇妙な理由により、ここにこの行を追加する必要があります。追加しない場合、ティック出来高に表示される値が正しくなくなります。関数には条件があることにご注意ください。これにより、高速測位システムの使用時の問題が回避され、システムのチャート上に時間外の奇妙なバーが表示されるのが防止されます。これは非常に奇妙な理由ですが、その他はすべて期待どおりに動作します。これは、Bidベースの資産を使用する場合とLastベースの商品を使用する場合の両方で、同じ方法でティックをカウントする新しい計算になります。

お気づきかもしれませんが、この計算はいたってシンプルです。しかし面白いのは、カスタム資産内のレート値をもう一度送信しなければならないことです。バーの閉じた後にです。その理由がわかりませんでした。すべてが非常に奇妙で、この送信はBidタイプが使用されている場合にのみ必要でした。非常に興味深いです。

先ほどの関数でもう1つお気づきのことがあるでしょう。今となっては、メトリクスのシステムはもはや存在しません。ある意味、気配値表示ウィンドウにティックが追加されたとき、私はこのシステムを削除しようと考えていました。これは、それぞれのバーを作るのに必要な時間を正確に見積もることができるからです。そのため、メトリックコードは削除しました


次のテストへの舞台設定

ここまでのすべての変更が実装されたので、本当のタスクに移ることができます。1分足バーファイルに存在するコンテンツのみに基づいてFOREX市場のティックをモデル化する方法を作成することです。信じていただきたいのは、この挑戦は非常に重要であると同時に、非常に興味深く、エキサイティングなものになるということです。物事を簡単にするために、C_FileTicksクラスを2つのクラスに分割します。しかし、これは単に質問を簡単にするためです。この分割は必要ありませんが、かなり面倒な作業を扱うことになるし、1000行を超えるようなクラスは好きではないので、C_FileTicksを2つのクラスに分割してみましょう。

このセクションでは、C_FileTicksクラスからティックりのモデリング部分を削除します。C_Simulationクラスは、問題なく表示(および実行)できるように、1分足バーのティックへの変換を担当します。C_Simulationクラスはリプレイシステムからは見えません。リプレイサービスでは、データは常に実ティックファイルから取得されます。実際、シミュレーションから登場することもあります。C_ReplayクラスからC_Simulationクラスにアクセスしようとしても、アクセスできません。C_Replayクラスは、C_ReplayクラスがMetaTrader 5端末にそれらを表示できるように、ファイルに存在する実際のティックを読み込むC_FileTicksクラスのみを見ることができるからです。

新しい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クラスをprivateに継承することになります。こうすることで、前述した通りのことが実現します。

しかし、C_FileTicksクラスのコードに少し変更を加える必要があります。これはC_Simulationクラスを継承しているからです。しかし、protectedと宣言されたデータを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クラス

さて、すべてを分割し、実際、どのようなタイプの市場にも対応できるようにしたところで、小さな、しかし非常に重要なポイントを決める必要があります。どのようなシミュレーションを作るのでしょうか。株式市場やFOREX市場と同じティックにしたいですか。この質問は混乱を招き、2つのシミュレーションモードを使わざるを得ないようです。次のことを考えてみましょう。あなたにとって(結局のところ、あなたはシステムをプログラムしているのだから)、この質問は比較的単純で、純粋に官僚的なものです。しかし、ユーザーにとっては何かと混乱するもので、多くの場合、データベースがFOREXと株式市場のどちらをシミュレーションしているのか、あまり知りたがりません。彼らは、システムが正常に動作すること、つまりデータをシミュレートすることを望みます。

しかし、1分足のバーファイルのデータを扱う場合、一見したところ、そのファイルのデータが外為市場のものなのか株式市場のものなのかを知る方法はありません。少なくとも、一見したところでは。しかし、これらのファイルをよく見て、その中のデータを比較すれば、あるパターンに気づくかもしれません。このパターンのおかげで、使用すべきプロットのタイプ(BidかLastか)を明確かつ効率的に定義することができます。そのためには、バーファイルの中身を見る必要があります。

プログラマーは、プログラムを特定のプロットモデルに適合させる作業を常に引き受けるべきです。プログラマーがユーザーの問題を解決してくれるため、ユーザーは1分バーファイルに存在するモデリングの種類を学習する必要はありません。何を言っているのか分からなくても、心配はいりません。奇妙な質問に見えるかもしれませんが、この特別な問題を解決するためのデータベースの仕組みを理解することが、シミュレーションシステムのコードを分割することにつながりました。これにより、より良い実装が可能になります。C_Simulationクラスに移る前に、BidタイプのプロットシステムとLastタイプのプロットシステムの見分け方を理解しましょう。

FOREXのデータをリプレイしているのか、株式市場のデータをリプレイしているのか、システムがどのように判断しているのか、ご理解いただけたと思います。もし理解できないのであれば、システムがどうしてこのような区別ができるのかを本当に理解できるまで、これまでの記事を読み直すことをお勧めします。しかし、リプレイに関する部分をすでに理解したのなら、重要な質問に移りましょう。1分足バーのファイルしか情報がないのに、FOREXや株式市場のデータで動くかどうかをシステムがどうやって知ることができるでしょうか。 

ティックを作成するためにシミュレーターを使用する必要があり、情報がFOREX市場や株式市場で使用されているものであるかどうかを調べるために、出来高を使用することができます。そう、まさにその出来高です。まさにその通りです。バーファイルがFOREX(Bid値を取引価格として使用)に属するか、株式市場(Las値を取引価格として使用)に属するかを決定する情報は、まさに出来高です。

もし、このことが理解できないのであれば、下の画像で1分足棒グラフのファイルの一部を見ることをお勧めします。


FOREX

図02:FOREX資産ファイル


株式市場

図03-株式市場の資産ファイル

実際の取引価格の目安としてBidとLastのどちらを使用しているかについては情報がありません。上の画像では、唯一の違いは出来高の値です。この値は画像で強調表示されているので、どこに違いがあるかがわかります。


記事の最後に思うこと

さて、もう1点見てみましょう。リプレイシステムの開発 - 市場シミュレーション(第11回):SIMULATORの誕生(I)",シミュレーションシステムの開発を始めたとき、私たちはランダムウォークをシミュレーションするために1分間バーにあるリソースの一部を使用しました。ファイルに報告されているように、これがその1分足での相場の動きでしょう。しかし、ランダムウォークを構築した場合、出来高がゼロであるか、ティックボリュームが非常に小さい場合を考慮した、「リプレイシステムの開発 - 市場シミュレーション(第15回)シミュレーターの誕生(V) - ランダムウォーク」稿を含め、この仕組みを構築する時点では、そのようなことは一切ありませんでした。しかし、市場や資産には独特の意味を持つものもあるため、そのようなケースもカバーする必要があります。これをしないと、シミュレーションを実行しようとすると、サービスがハングアップしたり、クラッシュしたりします。この問題はシミュレーションの場合に発生します。リプレイの場合、すべてが完全に調和している限り、すべてがうまく機能します。なぜなら、今後我々がおこなうような、エキゾチックな状況を扱う必要がないからです。

とりあえず、以前のことを思い出してください。注意深く検討する必要があるのは、FOREX市場のシミュレーターを作成する部分です。というのも、システムを理解するために説明しなければならない点の数が、これまで観測されてきたものとは大きく異なってくるからです。シミュレーションの計算を一部やり直さなければなりません。次の記事でお会いしましょう。



MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/11174

添付されたファイル |
Market_Replay_7vc22.zip (14387.78 KB)
リプレイシステムの開発 - 市場シミュレーション(第23回)FOREX (IV) リプレイシステムの開発 - 市場シミュレーション(第23回)FOREX (IV)
これで、ティックをバーに変換したのと同じ時点で作成がおこなわれます。こうすることで、変換プロセス中に問題が発生した場合、すぐにエラーに気づくことができます。これは、早送り中にチャート上に1分足を配置するコードと同じコードが、通常のパフォーマンス中に足を配置する位置決めシステムにも使用されるためです。言い換えれば、このタスクを担当するコードは他の場所には複製されません。このようにして、メンテナンスと改善の両方においてはるかに優れたシステムが得られます。
リプレイシステムの開発 - 市場シミュレーション(第21回):FOREX (II) リプレイシステムの開発 - 市場シミュレーション(第21回):FOREX (II)
FOREX市場で作業するためのシステムを構築し続けます。この問題を解決するためには、まず、前のバーを読み込む前にティックの読み込みを宣言しなければなりません。これによって問題は解決されますが、同時にユーザーは構成ファイルの構造に従わざるを得なくなります。これは個人的にはあまり意味がありません。なぜなら、構成ファイルの内容を分析し、実行する役割を担うプログラムを設計することで、ユーザーが必要な要素を好きな順番で宣言できるようになるからです。
リプレイシステムの開発 - 市場シミュレーション(第24回):FOREX (V) リプレイシステムの開発 - 市場シミュレーション(第24回):FOREX (V)
本日は、Last価格に基づくシミュレーションを妨げていた制限を取り除き、このタイプのシミュレーションに特化した新しいエントリポイントをご紹介します。操作の仕組みはすべて、FOREX市場の原理に基づいています。この手順の主な違いは、BidシミュレーションとLastシミュレーションの分離です。ただし、時間をランダム化し、C_Replayクラスに適合するように調整するために使用された方法は、両方のシミュレーションで同じままであることに注意することが重要です。これは良いことです。特にティック間の処理時間に関して、一方のモードを変更すれば、もう一方のモードも自動的に改善されるからです。
外国為替市場の季節性から利益を得る 外国為替市場の季節性から利益を得る
例えば、冬になると新鮮な野菜の値段が上がったり、霜が降りると燃料の値段が上がったりすることはよく知られていますが、同じようなパターンが外国為替市場にもあることを知っている人は少ないです。