English Русский Español Deutsch Português
preview
リプレイシステムの開発(第62回):サービスの再生(III)

リプレイシステムの開発(第62回):サービスの再生(III)

MetaTrader 5 | 28 4月 2025, 08:48
123 0
Daniel Jose
Daniel Jose

はじめに

前回の「リプレイシステムの開発(第61回):サービスの再生(II)」では、シミュレーションモードを使用している際に私たちのシステムで発生している問題について説明しました。これは、必ずしも開発中のアプリケーションに重大な障害があるというわけではなく、システム全体の応答速度に起因するものです。応答が追いつかず、すべての受信データをアプリケーションが適切に処理できなかったため、一定の調整が必要になりました。もちろん、私たちのサービスが理想的なシナリオ通りに動作していないとしても、現実にはそうした理想的な状況が存在すること自体まれであることも理解しています。

私が導き出した最適な解決策は、シミュレーションにおける最大制限値を調整することでした。ただし、この記事ではその変更がどのような影響をもたらすのかを慎重に検証し、なぜそのアプローチを選択したのかについても詳しく解説します。さらに、開発中のアプリケーション外で生成された実データや外部シミュレーションデータに直接関係する要素もあります。意外に思われるかもしれませんが、特に先物契約においては、1分間に異常な数のティックや取引が集中するケースがあります。こうした状況では、取引サーバーに接続していても、MetaTrader 5プラットフォームが価格の変動を処理・表示する速度に問題が生じることがあります。この問題に直面したことがない方であれば、MetaTrader 5を実行しているハードウェアの性能不足やOSの不具合が原因だと考えるかもしれません。しかし残念ながら、そうした推測は完全に誤りであり、コンピューティングに関する理解が不十分な人々によって広まった誤解に過ぎません。

実際の取引サーバーに接続している状態でも大量のティックデータの処理にプラットフォームが苦戦していることを考えると、それをリプレイする場合にはさらに深刻な状況となります。タイミングの精度が大きく損なわれ、完全な惨事となるでしょう。そのため、プラットフォームの処理能力の限界が露呈したり、新たな問題を引き起こしたりしないよう、実データや外部シミュレーションデータに対しても上限を設ける必要があります。それでは、新しいコードがどのように構成されるかを見ていきましょう。


新しいコンセプト、新しいシステム

おそらく、このセクションのタイトルは完全には自己説明的ではなく、これから実装しようとしている内容を十分に伝えていないかもしれません。しかし、まずはシステム内であらゆるシミュレーションの生成およびモデリングを担当するクラスに加えられた変更を分析していきます。シミュレーションクラスのコードを確認する前に、定義ファイルを最初に見ておく必要があります。そこに新しい行が追加されており、コード内のさまざまな箇所で参照されるからです。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define def_VERSION_DEBUG
05. //+------------------------------------------------------------------+
06. #ifdef def_VERSION_DEBUG
07.    #define macro_DEBUG_MODE(A) \
08.                Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + #A + " = " + (string)(A));
09. #else
10.    #define macro_DEBUG_MODE(A)
11. #endif
12. //+------------------------------------------------------------------+
13. #define def_SymbolReplay      "RePlay"
14. #define def_MaxPosSlider       400
15. #define def_MaxTicksVolume     2000
16. //+------------------------------------------------------------------+
17. union uCast_Double
18. {
19.    double    dValue;
20.    long      _long;                                 // 1 Information
21.    datetime _datetime;                              // 1 Information
22.    uint     _32b[sizeof(double) / sizeof(uint)];    // 2 Informations
23.    ushort   _16b[sizeof(double) / sizeof(ushort)];  // 4 Informations
24.    uchar    _8b [sizeof(double) / sizeof(uchar)];   // 8 Informations
25. };
26. //+------------------------------------------------------------------+
27. enum EnumEvents    {
28.          evHideMouse,               //Hide mouse price line
29.          evShowMouse,               //Show mouse price line
30.          evHideBarTime,             //Hide bar time
31.          evShowBarTime,             //Show bar time
32.          evHideDailyVar,            //Hide daily variation
33.          evShowDailyVar,            //Show daily variation
34.          evHidePriceVar,            //Hide instantaneous variation
35.          evShowPriceVar,            //Show instantaneous variation
36.          evSetServerTime,           //Replay/simulation system timer
37.          evCtrlReplayInit,          //Initialize replay control
38.                   };
39. //+------------------------------------------------------------------+

Defines.mqhファイルのソースコード

コードに追加されたのは行15です。この値がどこから来るのかについては、今は説明しません。シミュレーションコードで実際に使用する際に解説します。ここがその値が最初に使われる場所です。この変更について説明したので、次はティックのシミュレーションを担当するクラスのソースコードを見ていきましょう。以下に、変更が加えられたコードを示します。

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. #include "..\..\Defines.mqh"
005. //+------------------------------------------------------------------+
006. class C_Simulation
007. {
008.    private   :
009. //+------------------------------------------------------------------+
010.       int      m_NDigits;
011.       bool     m_IsPriceBID;
012.       double   m_TickSize;
013.       struct st00
014.       {
015.          bool  bHigh, bLow;
016.          int   iMax;
017.       }m_Marks;
018. //+------------------------------------------------------------------+
019. template < typename T >
020. inline T RandomLimit(const T Limit01, const T Limit02)
021.          {
022.             T a = (Limit01 > Limit02 ? Limit01 - Limit02 : Limit02 - Limit01);
023.             return (Limit01 >= Limit02 ? Limit02 : Limit01) + ((T)(((rand() & 32767) / 32737.0) * a));
024.          }
025. //+------------------------------------------------------------------+
026. inline void Simulation_Time(const MqlRates &rate, MqlTick &tick[])
027.          {
028.             for (int c0 = 0, iPos, v0 = (int)(60000 / m_Marks.iMax), v1 = 0, v2 = v0; c0 <= m_Marks.iMax; c0++, v1 = v2, v2 += v0)
029.             {
030.                iPos = RandomLimit(v1, v2);
031.                tick[c0].time = rate.time + (iPos / 1000);
032.                tick[c0].time_msc = iPos % 1000;
033.             }
034.          }
035. //+------------------------------------------------------------------+
036. inline void CorretTime(MqlTick &tick[])
037.          {
038.             for (int c0 = 0; c0 <= m_Marks.iMax; c0++)
039.                tick[c0].time_msc += (tick[c0].time * 1000);
040.          }
041. //+------------------------------------------------------------------+
042. inline int Unique(const double price, const MqlTick &tick[])
043.          {
044.             int iPos = 1;
045.             
046.             do
047.             {
048.                iPos = (m_Marks.iMax > 20 ? RandomLimit(1, m_Marks.iMax - 1) : iPos + 1);
049.             }while ((m_IsPriceBID ? tick[iPos].bid : tick[iPos].last) == price);
050.             
051.             return iPos;
052.          }
053. //+------------------------------------------------------------------+
054. inline void MountPrice(const int iPos, const double price, const int spread, MqlTick &tick[])
055.          {
056.             if (m_IsPriceBID)
057.             {
058.                tick[iPos].bid = NormalizeDouble(price, m_NDigits);
059.                tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits);
060.             }else
061.                tick[iPos].last = NormalizeDouble(price, m_NDigits);
062.          }
063. //+------------------------------------------------------------------+
064. inline void Random_Price(const MqlRates &rate, MqlTick &tick[])
065.          {
066.             for (int c0 = 1; c0 < m_Marks.iMax; c0++)
067.             {
068.                MountPrice(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (m_Marks.iMax & 0xF)), 0)), tick);
069.                m_Marks.bHigh = (rate.high == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bHigh;
070.                m_Marks.bLow = (rate.low == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bLow;
071.             }
072.          }
073. //+------------------------------------------------------------------+
074. inline void DistributeVolumeReal(const MqlRates &rate, MqlTick &tick[])
075.          {
076.             for (int c0 = 0; c0 <= m_Marks.iMax; c0++)
077.             {
078.                tick[c0].volume_real = 1.0;
079.                tick[c0].volume = 1;
080.             }
081.             if ((m_Marks.iMax + 1) < rate.tick_volume) for (int c0 = (int)(rate.tick_volume - m_Marks.iMax); c0 > 0; c0--)
082.                tick[RandomLimit(0, m_Marks.iMax - 1)].volume += 1;
083.             for (int c0 = (int)(rate.real_volume - m_Marks.iMax); c0 > 0; c0--)
084.                tick[RandomLimit(0, m_Marks.iMax)].volume_real += 1.0;
085.          }
086. //+------------------------------------------------------------------+
087. inline int RandomWalk(int In, int Out, const double Open, const double Close, double High, double Low, const int Spread, MqlTick &tick[], int iMode, int iDesloc)
088.          {
089.             double vStep, vNext, price, vH = High, vL = Low;
090.             char i0 = 0;
091.             
092.             vNext = vStep = (Out - In) / ((High - Low) / m_TickSize);
093.             for (int c0 = In, c1 = 0, c2 = 0; c0 <= Out; c0++, c1++)
094.             {
095.                price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -iDesloc : iDesloc));
096.                price = (price > vH ? vH : (price < vL ? vL : price));
097.                MountPrice(c0, price, (Spread + RandomLimit((int)(Spread | (m_Marks.iMax & 0xF)), 0)), tick);
098.                switch (iMode)
099.                {
100.                   case 1:
101.                      i0 |= (price == High ? 0x01 : 0);
102.                      i0 |= (price == Low ? 0x02 : 0);
103.                      vH = (i0 == 3 ? High : vH);
104.                      vL = (i0 ==3 ? Low : vL);
105.                      break;
106.                   case 0:
107.                      if (price == Close) return c0;
108.                   default:
109.                      break;
110.                }
111.                if (((int)floor(vNext)) >= c1) continue; else if ((++c2) <= 3) continue;
112.                vNext += vStep;
113.                vL = (iMode != 2 ? (Close > vL ? (i0 == 3 ? vL : vL + m_TickSize) : vL) : (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize)));
114.                vH = (iMode != 2 ? (Close > vL ? vH : (i0 == 3 ? vH : vH - m_TickSize)) : (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH)));
115.             }
116.             
117.             return Out;
118.          }
119. //+------------------------------------------------------------------+
120.    public   :
121. //+------------------------------------------------------------------+
122.       C_Simulation(const int nDigits)
123.          {
124.             m_NDigits       = nDigits;
125.             m_IsPriceBID    = (SymbolInfoInteger(def_SymbolReplay, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_BID);
126.             m_TickSize      = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
127.          }
128. //+------------------------------------------------------------------+
129. inline int Simulation(const MqlRates &rate, MqlTick &tick[], const int MaxTickVolume = def_MaxTicksVolume)
130.          {
131.             int    i0, i1, i2;
132.             bool   b0;
133.             
134.             m_Marks.iMax   = (MaxTickVolume <= 0 ? 1 : (MaxTickVolume >= def_MaxTicksVolume ? def_MaxTicksVolume : MaxTickVolume));
135.             m_Marks.iMax   = ((int)rate.tick_volume > m_Marks.iMax ? m_Marks.iMax : (int)rate.tick_volume - 1);
136.             m_Marks.bHigh  = (rate.open == rate.high) || (rate.close == rate.high);
137.             m_Marks.bLow   = (rate.open == rate.low) || (rate.close == rate.low);
138.             Simulation_Time(rate, tick);
139.             MountPrice(0, rate.open, rate.spread, tick);
140.             if (m_Marks.iMax > 10)
141.             {
142.                i0 = (int)(MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2));
143.                i1 = m_Marks.iMax - i0;
144.                i2 = (int)(((rate.high - rate.low) / m_TickSize) / i0);
145.                i2 = (i2 == 0 ? 1 : i2);
146.                b0 = (m_Marks.iMax >= 1000 ? ((rand() & 1) == 1) : (rate.high - rate.open) < (rate.open - rate.low));
147.                i0 = RandomWalk(1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, 0, i2);
148.                RandomWalk(i0, i1, (m_IsPriceBID ? tick[i0].bid : tick[i0].last), (b0 ? rate.low : rate.high), rate.high, rate.low, rate.spread, tick, 1, i2);
149.                RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, 2, i2);
150.                m_Marks.bHigh = m_Marks.bLow = true;
151. 
152.             }else Random_Price(rate, tick);
153.             if (!m_IsPriceBID) DistributeVolumeReal(rate, tick);
154.             if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
155.             if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
156.             MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
157.             CorretTime(tick);
158. 
159.             return m_Marks.iMax;
160.          }
161. //+------------------------------------------------------------------+
162. };
163. //+------------------------------------------------------------------+

C_Simulation.mqhのソースコード

この時点では、特にこのクラスを最後に変更してからかなりの時間が経過しているため、変更点にすぐには気づかないかもしれません。最後にこのクラスを扱ったのは、ランダムウォークに関する記事でした。ただし、生成されるティック数が若干減少したとしても、システムがタイミングの面で一定の一貫性を維持する必要があるため、このコードに小さな修正を加える必要がありました。

何が変更されたのかを理解してもらうために、1つの重要なポイントを取り上げます。コード全体には、プロセスを新しい手法に合わせるためだけに行われた小さな調整もいくつかありますが、それらは特別に注意を払う必要はありません。この中で最も重要な変更は、16行目に新しく追加された変数です。この変数は、もともとクラス内には存在していませんでした。ただし、この変数がクラス全体にどのような影響を与えるかを確認する前に、まずどこで初期化されているのかを見てみましょう。おそらく初期化はクラスのコンストラクタでおこなわれていると想像されるかもしれませんが、そうではありません。実際には、134行目と135行目の間で初期化されています。この2行で何が行われているかに注目してください。システムを改修する予定がある方にとっては、非常に重要なポイントになります。129行目では、ティックシミュレーションを生成する関数が定義されています。そして今回、シミュレートされる最大ティック数を指定する追加パラメータが新たに導入されました。Defines.mqhファイルに追加されたあの1行を覚えていますか。その定義が実際に使われている場所の1つが、まさにここです。では、実際に何が起きているのかを見てみましょう。これにより、コードを変更する場合に、その変更がシステムの挙動にどのような影響を与えるかを理解しやすくなります。ティックシミュレーションを実行する関数を呼び出すときは、シミュレートするティックの最大数も引数として渡す必要があります。ただし、この値には既定のデフォルト値が設定されているため、明示的に指定する必要はありません。指定された値が0以下だった場合、システムは最小値として1ティックを自動的に適用します。これは恣意的な仕様ではなく、外為取引では最小のティックボリュームが1であるというルールに基づいています。一方で、定義済みの最大値を超える値が指定された場合には、その入力値は無視され、システムに設定された上限が使用されます。この上限値はDefines.mqhヘッダーファイルで定義されており、システムでシミュレート可能なティックの最大数を定めています。0以上、最大値以下の範囲内であれば、その値がシミュレーションにおける最大ティック数として使用されます。このようにして、任意の範囲で柔軟に調整が可能となっています。

ここで重要な補足があります。この最大ティック数はランダムに選ばれたわけではありません。1分足をこの最大値(2000ティック)で分割すると、1ティックあたりの間隔は約30ミリ秒になります。この30ミリ秒という間隔は、プロット処理全体をスムーズで一貫性のある動きに保つための最適な値とされています。

もちろん、より大きな値を指定することもできますが、それによって実際にシミュレートされるティックの数が増えるわけではありません。あくまで許容される上限値が引き上げられるだけです。この説明は134行目に関係する部分ですが、実際に使用される最大ティック数は135行目で決定されます。135行目では、134行目で計算された値とバーに記録されているティック数とを比較します。そして、134行目の値がバーのティック数より小さい場合はその値が使用され、逆にバーのティック数のほうが少ない場合は、指定された入力値は無視されて、バー側のティック数が使用されます。

先ほど述べたように、これらの変更により、最大ティック数に関連するすべてのテストコードの見直しが必要となりました。その結果として、このクラス内のすべての関数や処理に対しても小規模な修正が加えられています。ただし、それぞれの変更は非常に単純なものであるため、ここで詳細に説明することはしません。不明点がある場合は、ランダムウォークに関する記事「リプレイシステムの開発 - 市場シミュレーション(第15回):シミュレーターの誕生(V) - ランダムウォーク」を参照してください。

さて、これで最初の問題は解決しました。今後は、シミュレーションを実行するたびに、1本のバーを作成するために必要なティックの最大数を調整できるようになりました。しかし、これだけでは目的を達成するには不十分です。忘れないでください:私たちはティックシミュレーションの処理自体は制御していますが、ユーザーが生成されるティック数を自由に減らせるような設定機能はまだ提供していません。つまり、まだ解決すべき問題が2つあるということです。そして、そのどちらにもシステムへのさらなる変更が必要です。

それでは、次の課題に取り組みましょう。次に考慮すべきは、実際のデータあるいは外部からシミュレートされたデータに含まれるティック数の上限についてです。この点を踏まえて、次のトピックに進みます。


実際の市場データに合わせた調整

タイトルでは実際の市場データのみが使用されることが示唆されていますが、これは完全には正しくありません。市場の動きを外部でシミュレーションし、それをファイルに保存したうえで、そのファイルを利用してバーの代わりにティックデータを提供することも可能です。これは、非常に有効かつ現実的なアプローチです。とはいえ、根本的な問題は前のセクションと同じです。すなわち、実際の市場データを扱う場合であっても、ティックの最大数に制限を設ける必要があるという点です。

ただし、前回とは異なり、今回の課題は扱うデータの性質により、はるかに複雑になります。もしリプレイ/シミュレーションの仕組みが特定の市場タイプだけに特化して設計されていれば、解決は比較的簡単だったでしょう。たとえば、ティックを読み込みながら、指定された時間ウィンドウ内の合計ティック数をチェックし、それがある閾値を超えていれば、超過分のティックを破棄するようシステムに指示することが可能です。この時間ウィンドウは1分足に相当します。そして、超過分を破棄した後は、残ったティックに基づき、ランダムウォークを使って動きを再生成するようシミュレーターに指示します。これにより、一定のウィンドウ内でティック数を制限することができます。しかしながら、ティックデータを読み込む際に、それがBID値なのかLAST値なのかを判別できないという問題があります。ここに、この課題の難しさが潜んでいます。というのも、どのチャートシステム(BIDベースかLASTベースか)が使用されているのかを特定できないからです。

まず最初に考えるべき問いは、この問題に対して最善の解決策は何かということです。ユーザーにチャートの種類を指定させるべきか、それともこの状況をコード側で自動的に処理すべきかということです。前者、すなわちユーザーに任せる方法は実装が非常に簡単です。しかし、その代わりに、間違って指定されるリスクがつきまといます。正直なところ、チャートの表現形式には2種類あり、それぞれが異なる市場モデルに対応していることを理解しているユーザーはどれほどいるでしょうか。多くのユーザーは、MetaTrader 5が3種類のサーバー(BID専用、ASK専用、LAST専用)をサポートしていることすら知りませんし、それぞれの資産に対して正しいプロット方式を設定する必要があるということも理解していないのが現実です。

このような潜在的な誤認のリスクを避けるため、私たちの実装では追加の工夫と作業が求められます。ただし、出発点ははっきりしています。それは、C_FileTicksクラスに定義されているLoadTicks関数です。ここで、この関数の元の実装を確認し、どのような変更を加えるべきかを検討していきましょう。以下がその関数です。

01.       datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
02.          {
03.             int      MemNRates,
04.                      MemNTicks;
05.             datetime dtRet = TimeCurrent();
06.             MqlRates RatesLocal[],
07.                      rate;
08.             bool     bNew;
09.             
10.             MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
11.             MemNTicks = m_Ticks.nTicks;
12.             if (!Open(szFileNameCSV)) return 0;
13.             if (!ReadAllsTicks()) return 0;         
14.             rate.time = 0;
15.             for (int c0 = MemNTicks; c0 < m_Ticks.nTicks; c0++)
16.             {
17.                if (!BuildBar1Min(c0, rate, bNew)) continue;
18.                if (bNew) ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
19.                m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
20.             }
21.             if (!ToReplay)
22.             {
23.                ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
24.                ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
25.                CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
26.                dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
27.                m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
28.                m_Ticks.nTicks = MemNTicks;
29.                ArrayFree(RatesLocal);
30.             }else SetSymbolInfos();
31.             m_Ticks.bTickReal = true;
32.                            
33.             return dtRet;
34.          }; 

C_FilesTicks.mqhソースコードの一部

行番号については気にしないでください。ここに記載されている行番号は、あくまで説明の参考用であり、実際のC_FileTicks.mqhファイル内の行を正確に示すものではありません。このファイルにはすでにいくつかの変更が加えられており、それらは後ほど確認していきます。それでは、実際のティックデータを読み込む際に何が起こるのかを見ていきましょう。

関数の開始時、行10および11では、読み込まれたティックの位置と、それらのティックを表すバーの現在の値が一時的に保存されます。呼び出し元が、ティックではなく既存のバーをリプレイの基準として使用するよう指定している場合は、これらの値はそれぞれ行27と28で上書きされます。これにより、システムは整合性を保ちつつ、リプレイ用のティックを待機する状態になります。

行12では、データファイルのオープンを試み、続く行13でファイル内のすべてのティック(文字通りすべて)を読み込みます。読み込みが成功すれば、すべてのティックがメモリにロードされます。しかし、時として単位時間内のティック数が、システムで定義された上限を超えることがあります。この時点では、私たちにはまだ何も対処できません。その理由は、どのチャートタイプが使われるのかがまだ確定していないということです。しかし、すべてのティックを読み込んだ後であれば、チャートタイプを特定でき、そこから処理に着手することが可能になります。

さて、ここからが面白い部分です。私たちは1分間の時間帯を分析しようとしています。行15からは、1分足の構築を目的としたループに入ります。これは、システムが必要に応じてバーにアクセスできるようにするための処理です。そして、この部分こそが、私たちが介入すべきポイントです。行17では、バー構築を担当する関数が呼び出されます。この関数では、バー内の動き(価格変動)を生成するために使用されたティックのボリュームが考慮されます。そして重要なのが行18です。ここの条件がtrueになると、バーは、あたかもバーファイルから直接読み込んだかのように扱われるようになります。まさにこのバー情報こそが、シミュレーションクラスへ渡すべきデータとなります。これについては、前回のトピックに戻り、行129を確認するとより明確になるはずです。

ここまでで、実装の方向性が見えてきたと思います。少なくとも初期段階では、ティック数がプログラム内で定義された上限を超えていることが判明した時点で、何らかのアクションを実行する必要があります。これに関しては、後ほどさらなる改良を加えていく予定です。

まずは簡単な部分からです。ティック数を確認し、必要に応じてシミュレーションクラスにランダムウォークを実行させることで、適切なティック数を確保して動きを生成させます。問題はここからです。シミュレーションクラスはティックを生成するものの、私たちは既に読み込まれたティックデータを、できる限り簡潔な方法で変更する必要があります。ティックの削除は、指定された時間ウィンドウ内で、上限を超えた場合に限りおこなう必要があります。また、別の小さな問題もあります。シミュレーション処理は、そのデータがリプレイ用に使われる場合にのみおこなうべきです。幸いなことに、これは非常に簡単に処理できます。呼び出し元からToReplayの値が通知されるため、それを確認すれば十分です。要するに、これは「楽勝」です。では、次に難関である不要なティックを効率的に上書きする方法を考えましょう。そのためには、前回提示した関数を修正し、別の関数に置き換える必要があります。

以下に示すのは、その実現に向けた最初の試みです(新しい機能の開発は常に試行錯誤の連続です)。

01. //+------------------------------------------------------------------+
02.       datetime LoadTicks(const string szFileNameCSV, const bool ToReplay, const int MaxTickVolume)
03.          {
04.             int      MemNRates,
05.                      MemNTicks,
06.                      nDigits,
07.                      nShift;
08.             datetime dtRet = TimeCurrent();
09.             MqlRates RatesLocal[],
10.                      rate;
11.             MqlTick  TicksLocal[];
12.             bool     bNew;
13.             
14.             MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
15.             nShift = MemNTicks = m_Ticks.nTicks;
16.             if (!Open(szFileNameCSV)) return 0;
17.             if (!ReadAllsTicks()) return 0;         
18.             rate.time = 0;
19.             nDigits = SetSymbolInfos(); 
20.             ArrayResize(TicksLocal, def_MaxSizeArray);
21.             m_Ticks.bTickReal = true;
22.             for (int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++)
23.             {
24.                if (!BuildBar1Min(c0, rate, bNew)) continue;
25.                if (bNew)
26.                {
27.                   if ((m_Ticks.nRate >= 0) && (ToReplay)) if (m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume)
28.                   {
29.                      nShift = MemShift;
30.                      C_Simulation *pSimulator = new C_Simulation(nDigits);
31.                      if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) < 0) return 0;
32.                      ArrayCopy(m_Ticks.Info, TicksLocal, nShift, 0, c1);
33.                      nShift += c1;
34.                      delete pSimulator;
35.                   }
36.                   MemShift = nShift;
37.                   ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
38.                };
39.                m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
40.             }
41.             ArrayFree(TicksLocal);
42.             if (!ToReplay)
43.             {
44.                ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
45.                ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
46.                CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
47.                dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
48.                m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
49.                m_Ticks.nTicks = MemNTicks;
50.                ArrayFree(RatesLocal);
51.             }else m_Ticks.nTicks = nShift;
52.                            
53.             return dtRet;
54.          };
55. //+------------------------------------------------------------------+

C_FileTicks.mqhクラスのソースコードの一部

ここでは、いくつかの項目が追加されていることに気づくかもしれません。また、特定のコマンドの実行順序にもいくつか変更が加えられています。しかしながら、このコード断片は一見すると解決策のように見えるものの、まだ小さな欠陥が残っています。それに入る前に、このC_FileTicksクラスのコード断片が、実際のティック数がシステム内部で定義された制限を超えないよう、どのように効果的に制御しているかを分析してみましょう。

この仕組みを実現するために、行6、7、11が追加されました。これらの行では、必要となる新しい変数を導入しています。そして行15では、読み込まれたティック数を追跡するための変数が初期化されます。行18と19は補助的な初期化をおこなうために追加されたもので、行20ではシミュレートされるティックを格納するメモリ領域が確保されます。このメモリは行41で解放される設計です。ただし、ここには行31に関わる小さな問題があり、これは最終バージョンで修正される予定です。今しばらくご辛抱ください。

行21は非常に重要な処理をおこなっていますが、この断片だけを見てもその理由は少し分かりにくいかもしれません。元のコードにおける行21の位置と比較してみれば、その本質が見えてきます。もし元の位置にこの処理を戻してしまうと、意図した動作を正しく果たせません。行24のBuildBar1Min関数が呼び出されるタイミングで、必要な条件が正しく整っていないためです。そのため、ここで示されているように処理の順番を調整する必要があります。

さて、本題である実装部分は、行27〜35に集中しています。このブロックは、分足においてティック数が定義された上限を超えた場合に、シミュレーションによって代替ティックを生成するための処理です。この上限値はアプリケーション内部で設定されています。

ここで注目すべきは、行27にある2つのチェックです。1つ目は、データがリプレイ用途で使われるか、あるいは既存のバーとして使用されるかを確認します。後者の場合はシミュレーションは不要です。2つ目のチェックは、ティック数が制限を超えているかを判定するものですが、これはインデックスが負の値になる可能性があるため、最初のチェックがこの問題を回避する役割も果たしています。その結果、ティック数のチェックは、直前に記録された現在のインデックスを対象とすることが保証されます。

ただし、少し疑問が生まれるかもしれません。行25の条件によって新しいバーが検出されたタイミングでこのチェックが実行されるのに、なぜ直前のバーのティック数を分析できるのかということです。もしここに気づいたなら素晴らしいです。コードの仕組みを本当に理解している証拠です。ただし、1つだけ見落としているかもしれない細かなポイントがあります。行39が実行されるまでは、システムは閉じたばかりのバーを参照し続けているのです。実際に新しいバーへの移行がおこなわれるのは行39のタイミングであり、これが実行順を厳密に制御する必要がある理由です。

では、ティックシミュレーションの流れに戻りましょう。行51を除けば、他のすべての部分はこれまでのシリーズで述べたとおりの動作を続けています。行51の動作は、シミュレーションで何が生成されるかに依存するため注意が必要です。ここには小さな問題が潜んでいますが、テストには大きな影響を与えません。

行29では、シミュレーションによって置き換える対象バーの先頭ティックにディスプレイスポインターを設定します。次に、行30でシミュレーターを初期化し、行31で実際のシミュレーションを開始します。ここで、シミュレーションに失敗した場合、行41が実行されないため、確保されたメモリが解放されないという欠陥があります。これはテスト段階では深刻な問題にはなりませんが、後ほど修正されるべき点です。ティックシミュレーションが成功すれば、行32でティックデータを格納する関数が呼び出されます。ちなみにこの処理はforループでも実装可能ですが、MQL5ライブラリのこのルーチンは高速なデータ転送に最適化されている可能性が高いため、こちらを使うほうが望ましいと考えられます。行33では、次に書き込むべき位置にポインタを更新し、行34でシミュレーションシステムを破棄します。

そして、行29の時点で常に正しい位置を参照できるよう、行36で位置情報を更新します。ただし、この一連の処理にも細かな欠陥がいくつか存在しており、それらについては次回の記事で解説します。さらに、シミュレーションフェーズにおけるほぼ見落とされがちなもう一つの問題も修正が必要です。


結論

1分足ごとのティック数を制限するという点では大きな前進がありましたが、それに伴って新たな問題が発生し、また今まで気づかなかった問題も表面化しました。この記事はすでに非常に内容が濃く、何が起きているのかを完全に理解するには丁寧に読み込む必要があるため、これ以上複雑な内容を追加するのは避けたいと思います。それでも、このコードの実装過程で現れた残りの不具合を自分で見つけて挑戦してみたい方のために、いくつかヒントをお伝えします。まず1つ目の問題は、シミュレーションすべきティックの最小数に関わるものです。これは非常に興味深い修正ポイントであり、この欠陥は、ティック数がある閾値を超えた際にシミュレーションをおこなおうとしたことで初めて明らかになりました。少し考えてみると、この問題がどのように起こるのか、きっと理解できるはずです。2つ目の問題は、シミュレートされた値をティック配列にコピーする際に発生します。このコピー処理によって、リプレイ用のバー生成システムが正しく動作しなくなることがあります。具体的には、非論理的または不自然なパターンが生成されたり、ある時間帯のティックが完全に消失してしまい、リプレイシステムの正確性と信頼性が損なわれるケースもあります。

次の記事を読む前に、ぜひこれらの問題の修正に挑戦してみてください。これは、問題の切り分け・解決方法や、自分自身のソリューションを設計・改善するスキルを磨くのに絶好のトレーニングになります。もちろん、次回の記事ではこれらの不具合をどのように修正するかを詳しく解説していきますので、ぜひ楽しみにしていてください。それでは、次の記事でお会いしましょう。


システムの欠陥を示すビデオ

すぐにこの問題を解決する方法を説明しますが、重大な問題ではないため、すぐには説明しません。この問題は、作成されたオブジェクトのロードまたはアンロードにのみ関連します。これは素晴らしい機会です。本当にプログラミングスキルをテストしたい場合は、方法を説明する前に問題を修正してみてください。あなたの解決策を私に示す必要はありませんが、私の解決策を読む前に試してみてください。これは、現在の学習レベルを評価するのに役立ちます。

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

添付されたファイル |
Anexo.zip (420.65 KB)
初級から中級へ:IF ELSE 初級から中級へ:IF ELSE
この記事では、IF演算子と、それに対応するELSEの使い方について解説します。この文は、あらゆるプログラミング言語において、最も重要かつ意義深いものです。しかし、その使いやすさにもかかわらず、使用経験や関連概念に対する理解がないと、時に混乱を招くことがあります。ここで提示されるコンテンツは、教育目的のみを目的としています。いかなる状況においても、提示された概念を学習し習得する以外の目的でアプリケーションを閲覧することは避けてください。
リプレイシステムの開発(第61回):サービスの再生(II) リプレイシステムの開発(第61回):サービスの再生(II)
この記事では、リプレイ/シミュレーションシステムをより効率的かつ安全に動作させるための変更点について解説します。また、クラスを最大限に活用したいと考えている方にも役立つ情報を取り上げます。さらに、クラスを使用する際にコードのパフォーマンスを低下させるMQL5特有の問題点を取り上げ、それに対する具体的な解決策についても説明します。
ニューラルネットワークの実践:最初のニューロン ニューラルネットワークの実践:最初のニューロン
この記事では、シンプルで控えめなもの、つまりニューロンの構築を始めます。ごく少量のMQL5コードでプログラムしますが、それでも私のテストではこのニューロンは見事に機能しました。とはいえ、私がここで何を言おうとしているのかを理解するには、これまでのニューラルネットワークに関する連載を少し振り返ってみる必要があります。
知っておくべきMQL5ウィザードのテクニック(第52回):ACオシレーター 知っておくべきMQL5ウィザードのテクニック(第52回):ACオシレーター
ACオシレーター(アクセラレーターオシレーター、Accelerator Oscillator)は、価格のモメンタムの「速度」だけでなく、その「加速」を追跡する、ビル・ウィリアムズによって開発されたインジケーターの一つです。最近の記事で取り上げたオーサムオシレーター(AO)と非常によく似ていますが、単なるスピードではなく加速に重点を置くことで、遅延の影響を回避しようとしています。本記事では、毎回のようにこのオシレーターからどのようなパターンが得られるかを分析し、ウィザード形式で構築されたエキスパートアドバイザー(EA)を通じて、それらが実際の取引においてどのような意味を持ち得るかを検証します。