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

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

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

はじめに

前回の「リプレイシステムの開発 - 市場シミュレーション(第22回):FOREX (III)」稿では、シミュレーターがLast値だけではなくBid価格に基づいて情報を生成できるように、システムにいくつかの変更を加えました。しかし、私はこれらの変更では満足できませんでした。理由は簡単です。コードが重複しているため、これは私にはまったく適していません。

その記事の中で私が不満を明らかにした箇所があります。

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

ただ、記事用のコードができていて、記事もほぼ完成していたので、そのまま放置していたのですが、これがすごく気になりました。コードが、ある状況では機能し、他の状況では機能しないのはおかしいです。コードをデバッグしてエラーの原因を探し出そうとしても、見つけることができませんでした。しかし、コードをしばらく放置してシステムフローチャートを確認した後(そう、コーディングを高速化するには常にフローチャートを使用するように努めるべきです)、コードの重複を避けるためにいくつかの変更を加えることができることに気づきました。さらに悪いことに、コードは実際に重複していました。これにより、解決できない問題が発生しました。しかし、解決策はあります。この問題の解決策からこの記事を始めます。この問題が存在すると、FOREX市場データを操作するシミュレーターコードを正しく作成できなくなる可能性があるからです。


ティック出来高の問題の解決

このトピックでは、ティック出来高の失敗を引き起こす問題がどのように解決されたかを示します。まず、以下に示すようにティック読み取りコードを変更する必要がありました。

datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
    {
        int      MemNRates,
                 MemNTicks;
        datetime dtRet = TimeCurrent();
        MqlRates RatesLocal[],
                 rate;
        bool     bNew;
        
        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if (!Open(szFileNameCSV)) return 0;
        if (!ReadAllsTicks(ToReplay)) return 0;         
        rate.time = 0;
        for (int c0 = MemNTicks; c0 < m_Ticks.nTicks; c0++)
        {
            if (!BuildBar1Min(c0, rate, bNew)) continue;
            if (bNew) ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
        }
        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分足に変換するコードの一部でしたが、現在は別のコードを使用します。その理由は、この呼び出しが複数の目的を果たし、その作業が繰り返しバーの作成にも使用されることです。これにより、クラス内でバーを作成するためのコードの重複が回避されます。

変換コードを見てみましょう。

inline bool BuildBar1Min(const int iArg, MqlRates &rate, bool &bNew)
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;
               if (m_Ticks.Info[iArg].last == 0.0) return false;
               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;
               if ((dClose == 0.0) || (m_Ticks.Info[iArg].bid == 0.0)) return false;
               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;
      }
      return true;                    
   }

C_Replayクラスで使用する要素を正しく作成できないため、コード内のすべての消された要素は削除されました。しかしその一方で、変換で何が起こったのかを呼び出し元に知らせるために、これらのポイントを追加する必要がありました。

当初、この関数はC_FileTicksクラスでprivateであったことにご注意ください。C_Replayクラスで使用できるようにアクセスレベルを変更しました。それにもかかわらず、これらの制限を超えすぎないようにするため、publicではなくprotectedにします。このようにして、C_Replayクラスで許可される最大レベルにアクセスを制限できます。覚えていらっしゃるとおり、最高レベルはC_Replayクラスです。したがって、C_Replayクラスでpublicとして宣言されたプロシージャと関数のみがクラスの外部からアクセスできます。システムの内部設計は、このC_Replayクラス内に完全に隠蔽される必要があります。

次に、新しいバー作成関数を見てみましょう。

inline void CreateBarInReplay(const bool bViewTicks)
   {
#define def_Rate m_MountBar.Rate[0]

      bool    bNew;
      double  dSpread;
      int     iRand = rand();
                                
      if (BuildBar1Min(m_ReplayCount, def_Rate, bNew))
      {
         m_Infos.tick[0] = m_Ticks.Info[m_ReplayCount];
         if ((!m_Ticks.bTickReal) && (m_Ticks.ModePlot == PRICE_EXCHANGE))
         {                                               
            dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
            if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last;
               m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
            }else   if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
               m_Infos.tick[0].bid = m_Infos.tick[0].last;
            }
         }
         if (bViewTicks) CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
         CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
      }
      m_ReplayCount++;
#undef def_Rate
   }

これで、ティックをバーに変換したのと同じ時点で作成がおこなわれます。こうすることで、変換プロセス中に問題が発生した場合、すぐにエラーに気づくことができます。これは、早送り中にチャート上に1分足を配置するコードと同じコードが、通常のパフォーマンス中に足を配置する位置決めシステムにも使用されるためです。言い換えれば、このタスクを担当するコードは他の場所には複製されません。このようにして、メンテナンスと改善の両方においてはるかに優れたシステムが得られます。ただし、上記のコードに重要な点を追加したことにもご注目ください。買値と売値のシミュレーションは、シミュレーションされたシステムを使用しており、シミュレーションされたデータが株式市場タイプである場合にのみおこなわれます。つまり、プロットがBidに基づいている場合、このシミュレーションは実行されなくなります。これは、次のトピックで設計を始める上で重要です。


Bidベースのプレゼンテーション(FOREXモード)のシミュレーションを開始しましょう。

以下では、C_Simulationクラスのみを取り上げます。これは、システムの現在の実装でカバーされていないデータをモデル化するためにおこないますが、まず1つの小さなことをおこなう必要があります。

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];
         if ((iRet = Simulation(rate[0], local)) < 0)
         {
            ArrayFree(local);
            delete pFileBars;
            return false;
         }
         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_Simulationクラスに伝える必要があるものがいくつか欠けています。コードを分析した後、関数の動作方法を変更することにしました。コードの重複を避けるために変更する必要があります。したがって、前の関数のことは忘れてください。機能しますが、実際には次のものを使用します。

int SetSymbolInfos(void)
   {
      int iRet;
                                
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, iRet = (m_Ticks.ModePlot == PRICE_EXCHANGE ? 4 : 5));
      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);
                                
      return iRet;
   }
//+------------------------------------------------------------------+
   public  :
//+------------------------------------------------------------------+
      bool BarsToTicks(const string szFileNameCSV)
      {
         C_FileBars      *pFileBars;
         C_Simulation    *pSimulator = NULL;
         int             iMem = m_Ticks.nTicks,
                         iRet = -1;
         MqlRates        rate[1];
         MqlTick         local[];
         bool            bInit = false;
                                
         pFileBars = new C_FileBars(szFileNameCSV);
         ArrayResize(local, def_MaxSizeArray);
         Print("Converting bars to ticks. Please wait...");
         while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
         {
            if (!bInit)
            {
               m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX);
               pSimulator = new C_Simulation(SetSymbolInfos());
               bInit = true;
            }
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
            if (pSimulator == NULL) iRet = -1; else iRet = (*pSimulator).Simulation(rate[0], local);
            if (iRet < 0) break;
            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;
         delete pSimulator;
         m_Ticks.bTickReal = false;
                                
         return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > 0));
      }

目標の観点からは、2つ目のオプションの方がはるかに効果的です。さらに、コードの重複を避けます。これを使用すると次の利点が得られるからです。

  • C_Simulationクラスの継承を削除:システムの柔軟性がさらに高まる
  • 資産データの初期化:以前は実際のティックを使用する場合にのみ実行されていた
  • グラフィック表示で使用される銘柄の幅が適切
  • C_Simulationクラスをポインタとして使用:クラスが作業を完了すると、クラスが占有していたメモリが解放されるため、システムメモリがより効率的に使用される
  • 関数の入口点と出口点が1つだけであることを保証
前回の記事と比べて一部変更がありますが、C_Simulationクラスの実装を続けましょう。C_Simulationクラスの開発の主な詳細は、システム内に任意の数のティックを含めることができるということです。これは(少なくとも現時点では)問題ではありませんが、多くの場合、高値と安値の間でカバーしなければならない範囲が、報告されるティック数や作成可能なティック数よりもはるかに大きくなることが難しい点です。これには、始値から始まりいずれかの極値に至るセクションと、いずれかの極値から始まり終値に至るセクションは含まれません。ランダムウォークを使用してこの計算を実装すると、非常に多くの場合、これは不可能になります。したがって、以前の記事で作成したランダムウォークを排除し、ティックを生成する新しい方法を開発する必要があります。先ほども言いましたが、FOREXの問題はそれほど明確ではありません。

このアプローチの問題は、多くの場合、2つの異なるメソッドを作成し、できるだけ調和して機能させる必要があることです。最悪の部分は、場合によっては、ランダムウォークシミュレーションが実際の資産で起こっていることに非常に近いことです。しかし、取引量が少ない場合(1分あたりの取引が500件未満)、ランダムウォークはまったく不適切です。この状況では、より特殊なアプローチを使用して、考えられるすべてのケースをカバーできます。最初におこなうことは(クラスを初期化する必要があるため)、クラスのコンストラクタを定義することです。そのコードは以下に示されています。

C_Simulation(const int nDigits)
   {
      m_NDigits       = nDigits;
      m_IsPriceBID    = (SymbolInfoInteger(def_SymbolReplay, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_BID);
      m_TickSize      = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
   }

ここでは、クラスのprivateデータを他の場所で検索しないように単純に初期化します。したがって、シミュレーションされている資産の構成ファイルで、プロットタイプを含むすべての設定が正しく設定されていることを確認してください。そうしないと、システム内で奇妙なエラーが発生する可能性があります。

クラスの基本的な初期化をおこなったので、次に進み始めることができます。解決する必要がある問題を見てみましょう。まず、無作為な時間値を生成する必要がありますが、この時間は1分足で生成されるすべてのティックを処理できる必要があります。これは実際には実装の最も単純な部分です。ただし、関数の作成を開始する前に、以下に示す特別なタイプのプロシージャを作成する必要があります。

template < typename T >
inline T RandomLimit(const T Limit01, const T Limit02)
   {
      T a = (Limit01 > Limit02 ? Limit01 - Limit02 : Limit02 - Limit01);
      return (Limit01 >= Limit02 ? Limit02 : Limit01) + ((T)(((rand() & 32767) / 32737.0) * a));
   }

この手順は具体的に何をもたらすのでしょうか。何が起こっているのかを理解せずにこの機能を見ると驚くかもしれません。そこで、この関数が実際に何をするのか、そしてなぜこれが奇妙に見えるのかをできるだけ簡単に説明しようと思います。

新しいコードでは、2つの極端な値の間の無作為な値を生成できるタイプの関数が必要です。場合によっては、この値をDoubleデータ型として形成する必要がありますが、他の場合には整数値が必要になります。同じ種類の因数分解を実行するために、実質的に同一のプロシージャを2つ作成するには、かなりの労力が必要になります。これを回避するには、同じ因数分解を使用してオーバーロードする必要があることをコンパイラに強制、またはむしろ指示します。これにより、コードでは同じ関数を使用できますが、実行可能形式では実際には2つの異なる関数が存在することになります。この宣言はこの目的で使用します。これは型(この場合は文字T)を定義します。コンパイラが型を設定する必要がある場合はどこでも、これを繰り返す必要があります。したがって、何かを混同しないように注意する必要があります。キャストの問題を回避するためにコンパイラに修正をおこなわせます。

したがって、常に同じ計算を実行しますが、使用される変数のタイプに応じて調整されます。どの型が正しいかを判断するのはコンパイラであるため、これをおこないます。このようにして、使用される型に関係なく、各呼び出しで擬似乱数を生成できますが、両方の境界の型が同じである必要があることにご注意ください。つまり、doubleと整数、long整数とshort整数を混在させることはできません。これではうまくいきません。これが、型のオーバーロードを使用する場合のこのアプローチの唯一の制限です。

ですが、まだ終わってはいません。C_Simulationクラスのコード内でマクロが生成されるのを避けるために、上記の関数を作成しました。次の手順、シミュレーションタイミングシステムの生成に進みましょう。この生成は以下のコードで確認できます。

inline void Simulation_Time(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 0, iPos, v0 = (int)(60000 / rate.tick_volume), v1 = 0, v2 = v0; c0 <= imax; c0++, v1 = v2, v2 += v0)
      {
         iPos = RandomLimit(v1, v2);
         tick[c0].time = rate.time + (iPos / 1000);
         tick[c0].time_msc = iPos % 1000;
      }
   }

ここでは時間を少し無作為にしてシミュレーションします。これは非常に難しく見えるかもしれません。ただし、信じてください。ここでの時間は無作為ですが、それでもC_Replayクラスが期待するロジックには対応していません。ミリ秒単位の値が正しく設定されていないためです。この調整は後ほどおこないます。ここでは、時間を1分バー以内で無作為に生成したいだけです。どうすればこれができるでしょうか。まず、60秒の時間(実際には60,000ミリ秒)を、生成する必要があるティック数で割ります。この値は、使用する制限範囲を示すため、重要です。その後、ループの各反復で、いくつかの単純な割り当てを実行します。ランダムタイマーを生成する秘密は、ループ内のこれら3行にあります。最初の行では、整数データを使用する呼び出しを生成するようにコンパイラに依頼します。この呼び出しは、指定された範囲の値を返します。次に、2つの非常に単純な計算を実行します。まず生成された値を分バータイムに適合させ、次に同じ生成値を使用して時間をミリ秒単位に適合させます。したがって、各ティックは完全に無作為な時間値を持ちます。この初期段階では時刻を修正しているだけであることにご注意ください。この設定の目的は、過度の予測可能性を回避することです。

続いて、価格をシミュレーションしてみましょう。もう一度言っておきますが、ここではBidベースのプロットシステムのみに焦点を当てます。次に、BidとLastの両方をカバーするシミュレーションを実行するためのより一般的な方法が得られるように、シミュレーションシステムをリンクします。ここではBidに焦点を当てます。この最初の手順でシミュレーションを作成するには、スプレッドを常に同じ距離に保ちます。実際に動作するかどうかをテストする前に、コードを複雑にすることはありません。この最初のシミュレーションは、いくつかのかなり短い関数を使用して実行されます。短い関数を使用して、すべてを可能な限りモジュール化します。この理由は後でわかります。

次に、Bidベースのシミュレーションを作成するためにおこなわれる最初の呼び出しを見てみましょう。

inline void Simulation_BID(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      bool    bHigh  = (rate.open == rate.high) || (rate.close == rate.high), 
      bLow = (rate.open == rate.low) || (rate.close == rate.low);
                                                        
      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < imax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), rate.spread, tick); 
         bHigh = (rate.high == tick[c0].bid) || bHigh;
         bLow = (rate.low == tick[c0].bid) || bLow;
      }
      if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick);
      if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(imax, rate.close, rate.spread, tick);
   }

上記の関数は非常に簡単に理解できます。ただし、最も難しい部分はBid値を無作為に構築することだと思われます。しかし、この場合でも、すべては非常に簡単です。バーの最大値と最小値の間の範囲で擬似ランダム値を生成します。ただし、値を正規化していることにご注意ください。これは、生成される価値が通常、価格の範囲外であるためです。だからこそ、それを正規化する必要があるのです。関数の残りは明確だと思います。

よく見ると、モデリング部分でよく言及される2つの関数があることがわかります。MOUNT_BIDおよびUNIQUEです。それぞれが特定の目的を果たします。Uniqueから始めましょう。以下がそのコードです。

inline int Unique(const int imax, const double price, const MqlTick &tick[])
   {
      int iPos = 1;
                                
      do
      {
         iPos = (imax > 20 ? RandomLimit(1, imax - 1) : iPos + 1);
      }while ((m_IsPriceBID ? tick[iPos].bid : tick[iPos].last) == price);
                                
      return iPos;
   }

この関数は、無作為なポジションを生成するときに、いずれかの制限値またはその他の価格が削除されるのを防ぎます。現時点では、制限内でのみ使用します。シミュレーションされたBid値またはシミュレーションされたLast値のいずれかを使用できることにご注意ください。現在はBidのみを使用しています。これがこの関数の唯一の目的です。制限値を上書きしないようにすることです。

ここで、Mount_BID関数を見てみましょう。そのコードを以下に示します。

inline void Mount_BID(const int iPos, const double price, const int spread, MqlTick &tick[])
   {
      tick[iPos].bid = price;
      tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits);
   }

この初期段階では、このコードは非常に単純で、純粋なプログラミングの美しさには及ばないものの、作業はずっと楽になります。これにより、複数の場所でコードを繰り返すことを避けることができ、最も重要なこととして、売値ポジションに配置する必要がある値を正規化することを忘れないようにすることができます。この正規化が実行されない場合、このAsk値をさらに使用するときに問題が発生します。ASK価格値は常にスプレッド値によって相殺されます。ただし、現時点では、このオフセットは常に一定です。というのは、今回が初めての実装であり、今からランダム化システムを実装すると、なぜ、どのようにスプレッド値を任意にするのかが全く不明になってしまうからです。

ここで表示されるスプレッド値は、実際には特定の1分足に表示される値です。各バーのスプレッドは異なる場合がありますが、他にも理解する必要があることがあります。実際の市場で何が起こるかに似たシステム(つまり、実際のティックファイルに含まれるデータ)を取得するためにシミュレーションを実行している場合、使用されているスプレッドは、1分足の形成に存在する値のうち小さい方であることがわかります。しかし、データが実際の市場で起こることと似ている場合も似ていない場合もある無作為なシミュレーションを実行している場合、そのスプレッドは任意の値を持つ可能性があります。ここでは、市場で起こり得ることを構築するという考えに固執します。したがって、スプレッド値は常にbarファイルで指定されたものになります。

システムが動作するために必要な関数がもう1つあります。この関数では、C_Replayクラスが正しいタイミング値を持つようにタイミングを設定する必要があります。このコードを以下に示します。

inline void CorretTime(int imax, MqlTick &tick[])
   {
      for (int c0 = 0; c0 <= imax; c0++)
         tick[c0].time_msc += (tick[c0].time * 1000);
   }

この関数は、指定された時間をミリ秒単位で調整するだけです。よく見ると、計算がファイルから実際のティックをロードする関数で使用されるものと同じであることがわかります。このモジュール方式のアプローチの理由は、実行された各機能の記録を保持すると興味深い場合があるためです。すべてのコードが相互接続されている場合、そのようなレコードの作成はさらに困難になります。ただし、この方法では、記録を作成して調査することができるため、特定のニーズを満たすために何を改善すべきか、何を改善すべきでないかを確認することができます。

重要な注意事項:この初期段階では、Lastベースのシステムの使用をブロックします。流動性が低い期間に資産を処理できるように、いくつかの場所を修正します。これは現時点では不可能ですが、後で修正する予定です。現在、Last価格に基づいてシミュレーションを実行しようとしても、システムでは実行できません。これは後で修正します。

これを確認するために、プログラミング手法の1つを使用します。非常に複雑でよく管理されたものになるでしょう。以下のコードをご覧ください。

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int imax;
                        
      imax = (int) rate.tick_volume - 1;
      Simulation_Time(imax, rate, tick);
      if (m_IsPriceBID) Simulation_BID(imax, rate, tick); else return -1;
      CorretTime(imax, tick);

      return imax;
   }

システムが最後のプロットモードを使用するたびに、エラーがスローされます。これは、Lastベースのシミュレーションを改善する必要があるためです。したがって、この複雑で高度なトリックを追加する必要がありました。Lastベースのシミュレーションを実行しようとすると、負の値が返されます。複雑な方法ではないでしょうか。

ただし、この記事を終える前に、Bidプロットモデリングの問題についてもう一度詳しく説明します。その結果、ランダム化の方法がわずかに改善されることになります。基本的に、無作為なスプレッド値を持つように1つの瞬間を変更する必要があります。これは、Mount_Bid関数またはSimulation_Bid関数で実行できます。ある意味大したことではありませんが、1分足ファイルで指定された最小スプレッド値を確保するために、以下に示す関数に変更を加えます。

inline void Simulation_BID(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      bool    bHigh  = (rate.open == rate.high) || (rate.close == rate.high), 
      bLow = (rate.open == rate.low) || (rate.close == rate.low);

      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < imax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (imax & 0xF)), 0)), tick);
         bHigh = (rate.high == tick[c0].bid) || bHigh;
         bLow = (rate.low == tick[c0].bid) || bLow;
      }
      if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick);
      if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(imax, rate.close, rate.spread, tick);
   }

ここではスプレッド値のランダム化を提供しますが、このランダム化はデモンストレーションのみを目的としています。必要に応じて、制限に関して少し異なる方法を実行することもできます。少し調整する必要があります。ここで、私がこのランダム化を使用していることを理解していただけると思います。これは一部の人にとっては少し奇妙に思えるかもしれませんが、実際にやっていることは次のとおりです。可能な限り最大の値を使用してスプレッドをランダム化できるようにしています。この値は、全ビットの一部のみを使用しているため、スプレッド値と1~16の範囲の値をビットごとに組み合わせる計算に基づいています。スプレッドがゼロの場合(そして、いくつかの時点では実際にゼロになる場合もある)、値1と2は実際にはスプレッドのランダム化を作成しないため、少なくとも3の値が得られることにご注意ください。これは、値1は始値が終値と等しいことのみを示し、値2は始値が終値と等しいか異なる可能性があることを示すためですが、この場合、実際に値を作成するのは値2です。その他のすべての場合、スプレッドにおけるランダム化の作成を扱います。

Mount_Bid関数にランダム化を適用しなかった理由が明らかになったと思います。これをおこなうと、barsファイルによって報告される最小スプレッドが正しくなくなる点がいくつか存在しますが、すでに述べたように、システムを自由に実験し、好みやスタイルに合わせて調整することができます。


結論

この記事では、コードの重複に関連する問題を解決しました。重複したコードを使用するとどのような問題が発生するかが明らかになったと思います。非常に大規模なプロジェクトでは、常にこれに注意する必要があります。このコードはそれほど大きくありませんが、この不注意により重大な問題が発生する可能性があります。

言及する価値のある最後の詳細は、実際のティックファイルでは、実際にはある種の「誤った」動きが存在する場合があるということです。しかし、ここではそんなことは起こりません。このような「誤った」動きは、価格の一方(BidまたはAsk)のみに変動が発生した場合に発生します。ただし、話を簡単にするために、そのような状況は無視しました。私の意見では、これは市場をシミュレーションするシステムにとってはあまり意味がありません。これでは操作の改善はもたらされません。AskなしのBidを変更するたびに、BidなしのAskを実行する必要があります。これは、実際の市場が要求するバランスを維持するために必要です。

これで、少なくともこの最初の試みに関しては、Bidベースのモデリングの問題は終わりました。将来的には、このシステムを別の方法で動作させるために変更を加える可能性があります。ただし、他の市場では十分ではないかもしれませんが、FOREXデータで使用すると非常にうまく機能することに気づきました。

添付ファイルを使用すると、現在の開発状態のシステムにアクセスできます。この記事ですでに述べたように、株式市場の資産を使用してモデリングを実行しようとすべきではなく、FOREX商品でのみ実行できます。どの金融商品でもリプレイできますが、為替取引資産のシミュレーションは無効になっています。次の記事では、流動性の低い環境でも機能するように株式市場のリプレイシステムを改善することでこの問題を修正します。これでシミュレーションの考察は終わりです。では、次の記事でお会いしましょう。

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

添付されたファイル |
Market_Replay_7vx23.zip (14388.45 KB)
リプレイシステムの開発 - 市場シミュレーション(第24回):FOREX (V) リプレイシステムの開発 - 市場シミュレーション(第24回):FOREX (V)
本日は、Last価格に基づくシミュレーションを妨げていた制限を取り除き、このタイプのシミュレーションに特化した新しいエントリポイントをご紹介します。操作の仕組みはすべて、FOREX市場の原理に基づいています。この手順の主な違いは、BidシミュレーションとLastシミュレーションの分離です。ただし、時間をランダム化し、C_Replayクラスに適合するように調整するために使用された方法は、両方のシミュレーションで同じままであることに注意することが重要です。これは良いことです。特にティック間の処理時間に関して、一方のモードを変更すれば、もう一方のモードも自動的に改善されるからです。
リプレイシステムの開発 - 市場シミュレーション(第22回):FOREX (III) リプレイシステムの開発 - 市場シミュレーション(第22回):FOREX (III)
このトピックに関する記事は今回で3回目になりますが、株式市場とFOREX市場の違いをまだ理解していない方のために説明しなければなりません。大きな違いは、FOREXでは、取引の過程で実際に発生したいくつかのポイントに関する情報がないというか、与えられないということです。
リプレイシステムの開発 - 市場シミュレーション(第25回):次の段階への準備 リプレイシステムの開発 - 市場シミュレーション(第25回):次の段階への準備
この記事では、リプレイ/シミュレーションシステム開発の第1段階を完了しました。この成果により、システムが高度なレベルに達したことを確認し、新機能の導入への道を開くことができました。目標は、システムをさらに充実させ、市場分析の調査開発のための強力なツールに変えることです。
リプレイシステムの開発 - 市場シミュレーション(第21回):FOREX (II) リプレイシステムの開発 - 市場シミュレーション(第21回):FOREX (II)
FOREX市場で作業するためのシステムを構築し続けます。この問題を解決するためには、まず、前のバーを読み込む前にティックの読み込みを宣言しなければなりません。これによって問題は解決されますが、同時にユーザーは構成ファイルの構造に従わざるを得なくなります。これは個人的にはあまり意味がありません。なぜなら、構成ファイルの内容を分析し、実行する役割を担うプログラムを設計することで、ユーザーが必要な要素を好きな順番で宣言できるようになるからです。