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

リプレイシステムの開発 - 市場シミュレーション(第12回):シミュレーターの誕生(II)

MetaTrader 5テスター | 19 12月 2023, 08:24
259 0
Daniel Jose
Daniel Jose

はじめに

前回の「リプレイシステムの開発 - 市場シミュレーション(第11回):シミュレーターの誕生(I)」では、1分足を使って市場の動きをシミュレートできるリプレイシミュレーションシステムが完成しました。ただし、おそらくこの資料を読んで、実際の市場の動きとあまり似ていないことに気づいたでしょう。その記事では、実際の市場で目にするものにさらに近づくために何を変更すべきかを示しました。しかし、単純な手法でいくら試行錯誤を繰り返しても、起こり得る相場の動きと同じものを作り出すことはできません。


実装の開始

すべてを必要なものにし、システムに複雑さを加えるために、乱数生成を使うことにします。そうすることで、物事が予測しにくくなり、リプレイ/シミュレーションシステムがより面白くなります。MQL5のドキュメントに記載されている乱数生成のヒントに従って、いくつかのステップを実行する必要があります。一見すると非常にシンプルです。心配する必要はありません。実際に非常にシンプルです。以下は、最初に追加するコードです。

void InitSymbolReplay(void)
        {
                Print("************** Market Replay Service **************");
                srand(GetTickCount());
                GlobalVariableDel(def_GlobalVariableReplay);
                SymbolSelect(def_SymbolReplay, false);
                CustomSymbolDelete(def_SymbolReplay);
                CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol);
                CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
                CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
                SymbolSelect(def_SymbolReplay, true);
        }


ここでは、ドキュメントのヒントに忠実に従います。このことは、疑似乱数の生成を初期化する関数srandを見れば確認できます。ドキュメントで説明されているように、例えば、呼び出しで固定値を使用する場合、

srand(5);

常に同じ番号の並びを受け取ります。こうすることで、ランダム生成を止め、「予測可能」なシーケンスを得ることができます。「予測可能」という言葉を引用符で囲んだのは、順序は常に同じだからです。ただし、全生成ループが完了するまでは、次の値がどうなるかは正確にはわかりません。シミュレーションのシーケンスが常に同じであるようなシミュレーションを作りたい場合、これはある意味で興味深くなります。その一方で、このアプローチは非常に簡単であるため、システムを使用して良い学習経験を得ることは不可能です。

テスターを使ってカスタムスタディを作成する場合、多数の異なるファイルを作成する意味はありません。ファイルを1つだけ作り、それを使ってすべてのランダム性を導入することができます。このため、srandを呼び出すときに固定値を指定するつもりはありません。チャンスに任せます。ただし、これは各自の判断に任されています。


もっと複雑なタスクの実行方法を試してみましょう。

まず最初にすることは、最小値を探すことから始めるという事実をなくすことです。これを知っていれば、すべてがとてもシンプルになります。新しいバーが開くのを待ち、売り操作を実行するだけです。もしオープンを上回るようであれば、実行しますが、これは訓練ではなく、不正行為です。

注意:一部のエキスパートアドバイザー(EA)はこのようなことを分析し、気づくことができます。これはストラテジーテスターで起こります。EAがこのことに気づくことができるという事実は、実行されたシミュレーションを無効にします。

そのためには、状況を複雑にしなければなりません。これから使うのは、とてもシンプルでありながら効果的な方法です。下のコードを見てみましょう。

inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                bool b1 = ((rand() & 1) == 1);
                                double p0, p1;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                p0 = (b1 ? rate.low : rate.high);
                                p1 = (b1 ? rate.high : rate.low);
                                Pivot(rate.open, p0, t0, tick);
                                Pivot(p0, p1, t0, tick);
                                Pivot(p1, rate.close, t0, tick, true);
                                v0 = (long)(rate.real_volume / (t0 + 1));
                                v1 = 0;
                                msc = 5;
                                v2 = ((60000 - msc) / (t0 + 1));
                                for (int c0 = 0; c0 <= t0; c0++, v1 += v0)
                                {
                                        tick[c0].volume_real = (v0 * 1.0);
                                        tick[c0].time = rate.time + (datetime)(msc / 1000);
                                        tick[c0].time_msc = msc % 1000;
                                        msc += v2;
                                }
                                tick[t0].volume_real = ((rate.real_volume - v1) * 1.0);
                                
                                return t0;
                        }


上記の関数が何をするかは、すべて以前と変わらないのでご心配いりません。唯一の変更点は、バーが最小値を探し始めるのか、最大値を探し始めるのかがわからなくなったことです。最初のステップは、ランダムに生成された値が偶数か奇数かをチェックすることです。これがわかれば、あとは値を入れ替えるだけで、ピボットポイントができあがりますが、ピボットポイントはこれまでと同じ方法で作成されることは念頭に置いておいてください。ただ1つわからないのは、バーがすでに最小値に達しているから上がっているのか、それともすでに最大値に達しているから下がっているのかということです。

これが始まりです。次のステップに進む前に、別の変更を加える必要があります。何が変わったのでしょうか。以前のバージョンでは、バーの始値と終値の間には通常9つのセグメントがありましたが、ほんの少しのコードで、この9つのセグメントを11のセグメントにすることができます。方法については、下のコードをご覧ください。

#define def_NPASS 3
inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                bool b1 = ((rand() & 1) == 1);
                                double p0, p1, p2;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                p0 = (b1 ? rate.low : rate.high);
                                p1 = (b1 ? rate.high : rate.low);
                                p2 = floor((rate.high - rate.low) / def_NPASS);
                                Pivot(rate.open, p0, t0, tick);
                                for (int c0 = 1; c0 < def_NPASS; c0++, p0 = (b1 ? p0 + p2 : p0 - p2)) Pivot(p0, (b1 ? p0 + p2 : p0 - p2), t0, tick);
                                Pivot(p0, p1, t0, tick);
                                Pivot(p1, rate.close, t0, tick, true);
                                v0 = (long)(rate.real_volume / (t0 + 1));
                                v1 = 0;
                                msc = 5;
                                v2 = ((60000 - msc) / (t0 + 1));
                                for (int c0 = 0; c0 <= t0; c0++, v1 += v0)
                                {
                                        tick[c0].volume_real = (v0 * 1.0);
                                        tick[c0].time = rate.time + (datetime)(msc / 1000);
                                        tick[c0].time_msc = msc % 1000;
                                        msc += v2;
                                }
                                tick[t0].volume_real = ((rate.real_volume - v1) * 1.0);
                                
                                return t0;
                        }
#undef def_NPASS


同じに見えるかもしれませんが、実は大きな違いがあります。中間点を表す変数を1つ追加しただけですが、この点を見つけたら、さらに2つのセグメントを追加することができます。この2つのセグメントを追加するために、ほとんど同じコードを実行し続けることになります。シミュレーションを作成するときにバーを形成する際に生じる複雑さは、コードを増やすのと同じ速度ではなく、すぐに増加することに注意してください。細かいことですが、定義がゼロになってはいけません。ゼロによる除算エラーが発生します。この場合、使用すべき最小値は定義にある1ですが、1から最大値までの任意の値を定義すれば、さらにセグメントを追加することができます。通常、それ以上のセグメントを作るほど広い動きはしないので、3という値で問題ない。

ここで何が起こったかを理解するために、以下の画像をご覧ください。

新しいセグメントを追加する前


すべてうまくいきましたが、振幅を範囲に分割できるバージョンを使うと、次のようなシナリオになる。

図02

変更後、バーの範囲を3で割り始めます。


複雑さが少し改善されたことに注目してください。しかし、3つ以上のセグメントに分けることに大きな利点は感じませんでした。物事はすでにかなり面白くなっていますが、システムはそれほど複雑さを生み出していません。よって、別のアプローチを取らなければなりません。これによってコードがより複雑になることはありません。コードを過度に複雑にすることなく、指数関数的な複雑さの増加を達成することがアイデアです。

そのために、まったく異なるアプローチを取ります。その前に説明に値することを考えてみましょう。こうすることで、問題解決へのアプローチを変える理由を本当に理解することができます。

前のステップでの変更に注意を払っていれば、最終的なコードに興味深いものがあることに気づいたかもしれません。一瞬、私たちはバーの本体全体をコントロールすることができ、バーを思いのままに操ることができます。他の時とは異なり、始値から高値または安値まで、比較的方向性のある動きがあります。バーの本体全体に取り組む必要があるときは、バーの内側ではほとんど仕事をしません。どんなに努力しても、いつも同じ状況に陥ってしまいます。しかし、よく見ると、常に2つの値があることに気づくはずです。これが起点と終点です。なぜこの瞬間に注目しなければならないのでしょうか。少し考えてみましょう。1分足のバーには6万ミリ秒あります。バーの最初に5ミリ秒のマージンを残しても、まだ多くの時間があります。簡単な計算をしてみれば、バーシミュレーションをもっと複雑にするために使えるはずの時間を、かなり浪費していることに気づくでしょう。

もし、始値から高値または安値に向かうまで1秒空け、そこから終値に向かうまで1秒空ければ、58秒間で複雑な動きを作り出すことができます。ただし、最後の1秒について「そこから終値に向かうまで」と言ったことに注目してください。これを正確に理解し、理解することが重要です。ほとんどの場合、何が起こっても、価格が最終的に終値に到達するまでの期間を常に確保しておく必要があります。

33ミリ秒強、つまり30Hzという、より長い時間で起こる動きに気づくでしょう。各ティックの最大継続時間を30ミリ秒に設定すると、資産の動きとよく似た動きになることがわかります。重要な詳細:この認識は非常に相対的なものです。ボラティリティが高いため、動きが非常に速い資産を取引するのは難しいと感じる人もいるからです。

このような理由から、リプレイ/シミュレーションシステムは良い学習とは言えません。実際に取引されたティックを含むファイルを使うのでなければです。このようなティックをシミュレートすると、すべての価格帯が訪れるという誤った印象を与える可能性があります。現在のところ、このシステムでは1分足バーをギャップを発生させるようにシミュレートすることはできませんが、実際の市場では、これらのギャップは非常に特定のタイミングで実際に発生します。このような時間帯は、取引を開始または決済するには非常に危険な時間帯です。注文が希望の価格以外で執行される可能性が非常に高く、ボラティリティが非常に高く、予想外の形ですべてが完璧に機能するため、また、単純に見逃される可能性も非常に高いためです。

常に最小限のティックを生成するメソッドを使うのかと思われるかもしれませんが、まだそのアプローチは使いません。ただし、これを覚えておく必要があります。シミュレーションによって実際の市場の動きを再現することはまったく不可能です。私たちにできるのは、起こりうる動きを推測することだけです。しかし、その前に、いくつかの具体的な問題を解決することに集中する必要があります。少し高度なトピックから始めますが、これがシミュレーターの基礎となります。しかし、その前に、いくつかの具体的な問題を解決することに集中する必要があります。


ティックがないなら、なぜサービスがアクティブなのでしょうか。

より現実に近い方向に進む前に解決しなければならない複雑な問題があるにもかかわらず、しばらく先送りされてきた、本当に解決しなければならない個々の問題を解決しなければなりません。これらの問題の第一は、プレビューバーを読み込まずにシステムを起動すると、コントロール指標にアクセスできないということです。このバグは以前からシステムに存在していたものですが、以前はプレビューバーが常に存在していたため、システム上必要な修正として後回しにしていました。では、この問題を解決してみましょう。そのためには、システム内の非常に特殊な箇所に、ちょっとした追加をしなければなりません。できるだけ作業を簡略化するために、このような方法をとりました。下をご覧ください。

                bool SetSymbolReplay(const string szFileConfig)
                        {
#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("Error occurred in line %d", iLine)), "Market Replay", MB_OK); return false; }
                                int     file,
                                        iLine;
                                string  szInfo;
                                char    iStage;
                                bool    bBarPrev;
                                MqlRates rate[1];
                                
                                if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
                                {
                                        MessageBox("Failed to open the\nconfiguration file.", "Market Replay", MB_OK);
                                        return false;
                                }
                                Print("Loading data for replay. Please wait....");
                                ArrayResize(m_Ticks.Rate, def_BarsDiary);
                                m_Ticks.nRate = -1;
                                m_Ticks.Rate[0].time = 0;
                                iStage = 0;
                                iLine = 1;
                                bBarPrev = false;
                                while ((!FileIsEnding(file)) && (!_StopFlag))
                                {
                                        switch (GetDefinition(FileReadString(file), szInfo))
                                        {
                                                case Transcription_DEFINE:
                                                        if (szInfo == def_STR_FilesBar) iStage = 1; else
                                                        if (szInfo == def_STR_FilesTicks) iStage = 2; else
                                                        if (szInfo == def_STR_TicksToBars) iStage = 3; else
                                                        if (szInfo == def_STR_BarsToTicks) iStage = 4; else
                                                        if (szInfo == def_STR_ConfigSymbol) iStage = 5; else
                                                                macroERROR(StringFormat("%s is not recognized in the system\nin line %d.", szInfo, iLine));
                                                        break;
                                                case Transcription_INFO:
                                                        if (szInfo != "") switch (iStage)
                                                        {
                                                                case 0:
                                                                        macroERROR(StringFormat("Couldn't recognize command in line %d\nof configuration file.", iLine));
                                                                        break;
                                                                case 1:
                                                                        if (!LoadPrevBars(szInfo)) macroERROR("");
                                                                        bBarPrev = true;
                                                                        break;
                                                                case 2:
                                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 3:
                                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                                        bBarPrev = true;
                                                                        break;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 5:
                                                                        if (!Configs(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);
                                if (m_Ticks.nTicks <= 0)
                                {
                                        MessageBox("No ticks to be used.\nClose the service...", "Market Replay", MB_OK);
                                        return false;
                                }
                                if (!bBarPrev)
                                {
                                        rate[0].close = rate[0].open =  rate[0].high = rate[0].low = m_Ticks.Info[0].last;
                                        rate[0].tick_volume = 0;
                                        rate[0].real_volume = 0;
                                        rate[0].time = m_Ticks.Info[0].time - 60;
                                        CustomRatesUpdate(def_SymbolReplay, rate, 1);
                                }
                                
                                return (!_StopFlag);
#undef macroERROR
                        }


まず、ローカル用に2つの新しい変数を定義します。そして、プレビューバーが読み込まれていないことを示すfalse値に初期化します。以前のバーが読み込まれると、この変数がtrueの値を示すようになります。こうすることで、システムは前のバーが読み込まれていることを知ることができ、最初の問題の一部を解決することができますが、使用されているティックを生成するファイルが読み込まれているかどうかを確認する必要があります。ティックがなければ、サービスを開始する意味はありません。したがって、サービスは停止されます。次に、ティックがあれば、前のバーが読み込まれたかどうかを確認します。なければ、空のバーを初期化します。この初期化をおこなわないと、たとえサービスが利用可能であっても、コントロール指標にアクセスすることができません。

しかし、上記のような修正を加えることで、すべては解決します。 


TICK VOLUMEの実装

次に修正すべきなのは、取引されたティックの量を示すシステムです。多くの人はチャート上にボリューム指標を表示しますが、今のところ実際に実装されているボリュームは本物のボリュームだけです。つまり、約定件数によるボリュームです。しかし、ティックボリュームを持つことも同様に重要です。この2つの違いは何だと思いますか。下の画像をご覧ください。

その中に2つのボリューム値が見えます。1つはティックボリューム、もう1つはボリューム(この場合は実際のボリューム)です。しかし、この画像を見て、実際のボリュームとティックボリュームの違いがおわかりでしょうか。おわかりでない場合、今こそそれを知る時です。

ボリュームまたは 実際のボリュームは基本的に、ある時点で取引された契約数です。資産によって異なりますが、常に値の倍数となります。たとえば、5未満の値での取引を認めない資産もあれば、端数のある値を受け入れる資産もあります。なぜこのようなことが可能なのかを理解しようとする必要はありません。この値はわかりやすいので、多くの人が使っているのかもしれません。ここで 実際のボリューム値を取得し、それに各契約の最小値を掛けると、金融ボリュームと呼ばれる別の値が得られます。MetaTrader 5はこの値を直接提供しませんが、ご覧いただいたように、取得するのは簡単です。従って、取引サーバーは、この金融ボリュームを取引ターミナルに報告する必要がないことを理解します。プログラマーまたはプラットフォームユーザーは、指定された計算を実装しなければなりません。

さて ティックボリュームはまったく別のボリュームです。これは単純な理由でバーのコンテンツでのみ提供されます。実際のボリュームを見ただけでは、取引中に何が起こったのかを知ることができないからです。ティックボリュームという追加情報が必要です。しかし、ティックボリュームはなぜバーのクエリでは利用可能で、ティックのクエリでは利用できないのでしょうか。ティックを要求すると、どのようなボリュームが表示されますか。このことに気づかれていない場合(あるいは、まだご覧になったことがない場合)、下の画像をご覧ください。

繰り返しますが、VOLUMEフィールドで指定された値はティックボリュームを表すものではありません。この値は実際のボリュームです。しかし、ティック要求時にティックボリュームが報告されない場合、どのようにしてそれを知ることができるのでしょうか。これは、バーのクエリを実行したときにのみ表示されます。大事なのは、サーバーは、「金融ボリュームを提供する必要がないことを理解しているのと同じように、ティックボリュームを計算できることを理解しているということです。これは、実際に取引されているティックにアクセスできないときにバーをリクエストした場合とは異なります。

まだあやふやでしょうか。実際に取引されたティックのデータがあれば、ティックのボリュームを計算することができます。方法については、不思議な方程式でもあるのでしょうか。何度やっても値が合わないことがあります。皆さん落ち着いてください。魔法の公式はありません。ティックボリュームの本当の意味を理解していないのかもしれません。今回と前回の記事では、分足内の動きをモデル化する方法を用いました。この動きはすべての価格に影響を与えますが、実際に作成されるティックボリュームは、1分足バーで報告されるティックボリュームよりもはるかに少なくなります。

でも、なぜでしょうか。ご安心ください。次回は、同じティックボリュームを実際にモデル化するので、より分かりやすくなります。ティックボリュームとは何か、おわかりいただけたでしょうか。ティックボリュームとは、特定のバー内で実際に発生した取引の数です。ここでの平均的なボリュームは約150です。実際、平均は12,890前後であることが多くなります。

読者はティックボリュームがどのように計算できるかを疑問に思っているかもしれません。とても簡単です。私たちのシステムがこの計算を実行できるか見てみましょう。これを理解するには、実際に計算を見る必要があるからです。

現在、この計算は異なる理由から2か所でおこなわれています。1か所目は以下の通りです。

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


この段階で、バー内に存在するティックのボリュームを計算します。2か所目は以下の通りです。

inline int Event_OnTime(void)
                        {
                                bool    bNew;
                                int     mili, iPos;
                                u_Interprocess Info;
                                static MqlRates Rate[1];
                                static datetime _dt = 0;
                                datetime tmpDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                
                                if (m_ReplayCount >= m_Ticks.nTicks) return -1;
                                if (bNew = (_dt != tmpDT))
                                {
                                        _dt = tmpDT;
                                        Rate[0].real_volume = 0;
                                        Rate[0].tick_volume = 0;
                                }
                                mili = (int) m_Ticks.Info[m_ReplayCount].time_msc;
                                do
                                {
                                        while (mili == m_Ticks.Info[m_ReplayCount].time_msc)
                                        {
                                                Rate[0].close = m_Ticks.Info[m_ReplayCount].last;
                                                Rate[0].open = (bNew ? Rate[0].close : Rate[0].open);
                                                Rate[0].high = (bNew || (Rate[0].close > Rate[0].high) ? Rate[0].close : Rate[0].high);
                                                Rate[0].low = (bNew || (Rate[0].close < Rate[0].low) ? Rate[0].close : Rate[0].low);
                                                Rate[0].real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
                                                Rate[0].tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
                                                bNew = false;
                                                m_ReplayCount++;
                                        }
                                        mili++;
                                }while (mili == m_Ticks.Info[m_ReplayCount].time_msc);
                                Rate[0].time = _dt;
                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                                iPos = (int)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                if (Info.s_Infos.iPosShift != iPos)
                                {
                                        Info.s_Infos.iPosShift = (ushort) iPos;
                                        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                }
                                return (int)(m_Ticks.Info[m_ReplayCount].time_msc < mili ? m_Ticks.Info[m_ReplayCount].time_msc + (1000 - mili) : m_Ticks.Info[m_ReplayCount].time_msc - mili);
                        }

この段階で同じことをする、つまりティックボリュームを計算します。本当にそうなのでしょうか。はい、そうです。ティックボリュームは、約定した取引を示すティックのみを実際に含めて計算されます。つまり、1回の操作で1ティックになります。BIDまたはASKフラグが有効なティックは計算に参加せず、SELLまたはBUYフラグがあるティックのみが計算されます。しかし、これらのフラグが有効になるのは、価格値または実際のボリュームがゼロより大きいときだけなので、フラグを確認する必要はありません。

メモ:FXでは、これは変わりますが、これについてはFXに関連した特別記事で説明します。

今後、リプレイ/シミュレーションシステムにはティックボリュームが表示されることになります。しかし、1つだけ細かいことがあります。現在、ティックをシミュレートするためにバーを使用する場合、ボリュームは常にバーファイルで指定されたものとは異なります。次回はこれを修正します。私たちが何をしなければならないかを冷静に説明するために、別の記事が必要です。


基準点の設定

次に解決しなければならない問題は(実際には問題ではありませんが)、各ポジションユニットが何を表しているかをシステムに認識させることです。問題は、これまでこのシステムは、ユーザーが指定した位置決めを実行するのに、非常に不適切な方法を使っていたことです。ティックデータを得るために複数のファイルを使用することが可能になると、以前のシステムでは全く受け入れがたい状況になります。このように、コントロール指標に表示されるものと、リプレイで表示されるものとの変換に問題があります。 

この問題を解決するには、システム読み込みである行を削除する必要があります。

                bool LoadTicksReplay(const string szFileNameCSV, const bool ToReplay = true)
                        {
                                int     file,
                                        old,
                                        MemNRates,
                                        MemNTicks;
                                string  szInfo = "";
                                MqlTick tick;
                                MqlRates rate,
                                        RatesLocal[];
                                
                                MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
                                MemNTicks = m_Ticks.nTicks;
                                if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                        ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary);
                                        old = m_Ticks.nTicks;
                                        for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
                                        if (szInfo != def_Header_Ticks)
                                        {
                                                Print("File ", szFileNameCSV, ".csv is not a traded tick file.");
                                                return false;
                                        }
                                        Print("Loading data for replay. Please wait...");
                                        while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag))
                                        {
                                                ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                                                szInfo = FileReadString(file) + " " + FileReadString(file);
                                                tick.time = StringToTime(StringSubstr(szInfo, 0, 19));
                                                tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                                tick.bid = StringToDouble(FileReadString(file));
                                                tick.ask = StringToDouble(FileReadString(file));
                                                tick.last = StringToDouble(FileReadString(file));
                                                tick.volume_real = StringToDouble(FileReadString(file));
                                                tick.flags = (uchar)StringToInteger(FileReadString(file));
                                                if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc))
                                                        m_Ticks.Info[old].volume_real += tick.volume_real;
                                                else
                                                {                                                       
                                                        m_Ticks.Info[m_Ticks.nTicks] = tick;
                                                        if (tick.volume_real > 0.0)
                                                        {
                                                                m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0);
                                                                rate.spread = (ToReplay ? m_Ticks.nTicks : 0);
                                                                m_Ticks.Rate[m_Ticks.nRate] = rate;
                                                                m_Ticks.nTicks++;
                                                        }
                                                        old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old);
                                                }
                                        }
                                        if ((!FileIsEnding(file)) && (!_StopFlag))
                                        {
                                                Print("Too much data in the tick file.\nCannot continue...");
                                                FileClose(file);
                                                return false;
                                        }
                                        FileClose(file);
                                }else
                                {
                                        Print("Tick file ", szFileNameCSV,".csv not found...");
                                        return false;
                                }
                                if ((!ToReplay) && (!_StopFlag))
                                {
                                        ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
                                        ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
                                        CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
                                        m_dtPrevLoading = m_Ticks.Rate[m_Ticks.nRate].time;
                                        m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
                                        m_Ticks.nTicks = MemNTicks;
                                        ArrayFree(RatesLocal);
                                }
                                return (!_StopFlag);
                        };


この例外をスローすることで、変数「spread」が解放され、別の機会に適宜調整することができます。まだその必要はないので、この記事では取り上げませんが、これが完了したら、変換を担うシステムを修正しなければなりません。なぜなら今後、ポジション制御システムは常に無効なポイントを示すからです。より正確には、ユーザーが望んでいることとは異なる点でしょう。

正しく変換するためには、非常に特殊な手順を変更する必要があります。こちらです。

                long AdjustPositionReplay(const bool bViewBuider)
                        {
                                u_Interprocess  Info;
                                MqlRates        Rate[def_BarsDiary];
                                int             iPos,
                                                nCount;
                                
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return 0;
                                iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
                                Rate[0].time = macroRemoveSec(m_Ticks.Info[iPos].time);
                                if (iPos < m_ReplayCount)
                                {
                                        CustomRatesDelete(def_SymbolReplay, Rate[0].time, LONG_MAX);
                                        if ((m_dtPrevLoading == 0) && (iPos == 0))
                                        {
                                                m_ReplayCount = 0;
                                                Rate[m_ReplayCount].close = Rate[m_ReplayCount].open = Rate[m_ReplayCount].high = Rate[m_ReplayCount].low = m_Ticks.Info[iPos].last;
                                                Rate[m_ReplayCount].tick_volume = Rate[m_ReplayCount].real_volume = 0;
                                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                                        }else
                                        {
                                                for(Rate[0].time -= 60; (m_ReplayCount > 0) && (Rate[0].time <= macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)); m_ReplayCount--);
                                                m_ReplayCount++;
                                        }
                                }else if (iPos > m_ReplayCount)
                                {
                                        if (bViewBuider)
                                        {
                                                Info.s_Infos.isWait = true;
                                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                        }else
                                        {
                                                for(; Rate[0].time > m_Ticks.Info[m_ReplayCount].time; m_ReplayCount++);
                                                for (nCount = 0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++);
                                                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount);
                                        }
                                }
                                for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) Event_OnTime();
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                Info.s_Infos.isWait = false;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);

                                return Event_OnTime();
                        }


上記の変換は、以前の記事で紹介したバージョンとは大きく異なります。これは、実際にユーザーが設定したパーセンテージ値をコントロール指標と位置決めシステムに変換するためで、チケットがどのように編成されているかは関係ありません。このプロシージャは、正しいポイントを検索し、そのポイントからティックで見つかったデータの提示を開始します。

これを正確におこなうには、まず計算を行い、希望する位置がパーセンテージでどのあたりにあるかを決定します。このポジションは非常に重要です。もし値が低ければ、ある点まで戻らなければならないことを意味します。そして、その点に近づくまで情報を削除します。通常、いくつかの追加データは常に削除されますが、これはプロセスの一部であり、後でこのデータを戻すことになります。確かに、データシリーズの最初に戻っているのかもしれませんが、そうでない場合は、カウンタをリセットしてパーセンテージ値に近い点に戻します。この特別なラインは、いつも私たちが実際に望んだよりも遠くに戻ってしまうという問題を解決してくれます。これがないと、プレビューバーが正しく表示されません。後方システムは前方システムよりも複雑です。前方については、ユーザーが作成中のバーを見たいかどうかを確認するだけです。バーは必要に応じて表示され、それ以外の場合、システムはパーセンテージ値で示される点にジャンプします。ほとんどの場合、パーセンテージの値と実際のポジションの間で微調整をする必要があります。ただし、物事は非常に迅速におこなわれます。実際の値がパーセンテージの値に近ければ、実質的に瞬時に移行します。しかし、値が遠くにある場合は、小さなアニメーションが表示され、バーがどのように作られているかが示されます。


記事の最後に思うこと

システムはずっと使いやすくなったように見えますが、バー建設表示モードで作動させると、奇妙なことに気づくかもしれません。これらの珍しいことは下のビデオで見ることができます。しかし、コード上のいくつかの場所を変更する必要があり、また、これらのことが突然出てきたと思われたくなかったので、私は「バグ」を残すことにしました。おそらく主な理由は、次回、このシステムをシミュレーターとしてより適したものにする方法を紹介する事だと思います。私がなぜシミュレーターをプログラムしたのかは次回の記事で紹介するので、聞かないでください。

では、ビデオをご覧ください。私が何が起こっているかを認識していることを知ってください。



ここで使用したファイルは添付ファイルにあります。また、同じ日に取引された1分足とティックの両方を表示する追加ファイルもあります。両方の構成を実行し、結果を確認してください。ただ、まずはチャート上で何が起こっているかを理解する必要があります。

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

添付されたファイル |
リプレイシステムの開発 - 市場シミュレーション(第14回):シミュレーターの誕生(IV) リプレイシステムの開発 - 市場シミュレーション(第14回):シミュレーターの誕生(IV)
この記事ではシミュレーターの開発段階を続けます。 今回は、ランダムウォークタイプの動きを効果的に作成する方法を見ていきます。このような動きには非常に興味をそそられます。資本市場で起こるすべてのことの基礎がそれによって形成されるためです。さらに、市場分析をおこなう上で基本となるいくつかの概念についても理解を深めていきます。
リプレイシステムの開発 - 市場シミュレーション(第11回):シミュレーターの誕生(I) リプレイシステムの開発 - 市場シミュレーション(第11回):シミュレーターの誕生(I)
バーを形成するデータを使うためには、リプレイをやめてシミュレーターの開発に着手しなければなりません。難易度が最も低い1分バーを使用します。
MQL5における座標降下法を用いたエラスティックネット回帰 MQL5における座標降下法を用いたエラスティックネット回帰
この記事では、過学習を最小化すると同時に、有用な予測因子と予後予測力の低い予測因子を自動的に分離するエラスティックネット回帰の実用的な実装を探求します。
リプレイシステムの開発 - 市場シミュレーション(第10回):リプレイで実データのみを使用する リプレイシステムの開発 - 市場シミュレーション(第10回):リプレイで実データのみを使用する
ここでは、リプレイシステムで、調整されているかどうかを気にすることなく、より信頼性の高いデータ(取引されたティック)を使用する方法を見ていきます。