English Русский 中文 Español Deutsch Português
preview
リプレイシステムの開発 - 市場シミュレーション(第18回):ティックそしてまたティック(II)

リプレイシステムの開発 - 市場シミュレーション(第18回):ティックそしてまたティック(II)

MetaTrader 5テスター | 12 1月 2024, 10:31
216 0
Daniel Jose
Daniel Jose

はじめに

前回の「リプレイシステムの開発 - 市場シミュレーション(第17回):ティックそしてまたティック(I)」では、気配値表示システムにティックチャートを表示する機能を追加しました。これは非常に前向きな展開でしたが、その記事の中で、私たちのシステムにはいくつかの欠点があると述べました。そのため、欠点が修正されるまでサービスの一部の機能を無効にすることにしました。ここで、ティックチャートの表示を開始したときに発生した多くのエラーを修正します。

私にとって最も顕著で、おそらく最も迷惑なバグの1つは、1分足の作成に必要なシミュレーション時間に関連しています。リプレイ/シミュレーションサービスをフォローしてテストしている人は、タイミングが理想とは程遠いことに気づいたかもしれません。資産に一定の流動性がある場合、これはさらに顕著になり、実際の取引が数秒間失われる可能性があります。私は当初からこれに気を配り、リプレイ/シミュレーションサービスの使用体験を実際の資産の取引体験と同様のものにしようと努めました。

明らかに、現在の指標は1分足を作成するのに理想的な時間からは程遠いです。それが最初に修正することです。同期の問題を解決するのは難しくありません。難しそうに思えるかもしれませんが、実際はとても簡単です。前回の記事の目的は、チャート上の1分足を作成するために使用されたティックデータを気配値ウィンドウに転送する方法を説明することであったため、必要な修正はおこないませんでした。

タイマーを修正すると決めた場合、ファイルに保存されている実際のティックデータが気配値表示ウィンドウにどのように適用されるかを知りたい人にとっては理解しにくいでしょう。したがって、気配値表示ウィンドウでティックを有効にする方法だけに焦点を当てることで、このプロセスにどのように取り組むかが明確になったと思います。重要な詳細の1つは、これをおこなう方法に関する参考文献が他に見つからなかったことです。唯一の参考となるのはドキュメント自体であり、検索していると、同様にこれをおこなう方法を知りたがっているコミュニティフォーラムの人々さえ見つけました。しかし、プロセスがどうあるべきかを理解するのに本当に役立つ答えは見つかりませんでした。そのため、前回の記事は少し奇妙な形で終わったようで、そこで言及されている問題を解決する方法が明確ではないように見えました。

しかし、説明するのがはるかに難しい質問があるため、ここではまだ完全ではありませんが、実際にこれに対処します。多くの場合、実装は比較的単純です。全く異なる点でも何らかの関連性がある点を1つの記事ですべて説明すると、非常に混乱してしまいます。説明する代わりに、理解のプロセス全体がさらに複雑になる可能性があります。

各記事に対する私の考えは、人々に MetaTrader 5プラットフォームと MQL5言語を学習し、深く探究するよう説明し、奨励することです。これは、どこかに配布されているコードで見られるものをはるかに超えています。MQL5やMetaTrader 5が、誰もがそれを使っておこなうこと以外に何のメリットももたらさないかのように、いつも同じことをするのではなく、創造力と意欲を持って、これまでに選択したことのない道を探求してほしいと心から願っています。記事に戻りましょう。


1分足作成時間の修正を実施する

タイマーから始めましょう。これを修正するには、コード全体の小さな詳細を1つだけ変更します。この変更は以下のコードに示されています。

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);
                                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.time_msc = (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));                                       
                                        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
                        }

すべて準備できました。タイマーがより正確に動作するようになりました。どうしてそんなことがあり得るのかと疑問に思われ、理解できない🤔と思われるかもしれません。単純に行を削除し(すでに削除されています)、それをちょっとした計算に置き換えるだけでタイマーの問題は完全に解決されますが、それだけではありません。時間の値を削除してゼロのままにすることもできます。

これにより、気配値チャートにティックを追加する際に、いくつかのマシンサイクルが節約されます。しかし(この「でも」というところが本当に疑問に思いますが)、MetaTrader 5でプロットされる1分足を作成するときに、追加の計算を実行する必要があります。その結果、計算のためだけに数マシンサイクルを費やす必要があります。私たちのやり方なら、コストが大幅に削減されます。

この変更により、すぐに別の変更を実装できます。

inline void ViewTick(void)
                        {
                                MqlTick tick[1];

                                tick[0] = m_Ticks.Info[m_ReplayCount];
                                tick[0].time_msc = (m_Ticks.Info[m_ReplayCount].time * 1000) + m_Ticks.Info[m_ReplayCount].time_msc;
                                CustomTicksAdd(def_SymbolReplay, tick);
                        }

ファイルから実際のティックを読み込むときに計算が実行されるため、以前のリモート計算はもう必要ありません。前回の記事でこれをおこなっていたら、多くの人はなぜ相場監視チャートにティックが表示されるのか理解できなかったでしょう。しかし、すでに述べたように、これは今ではかなり明確になっているように思えます。変更がよりスムーズになるという単純な事実により、変更は誰にとってもはるかに理解しやすくなります。

ここで、少し不安になるかもしれない質問が出て来ます。

取引が数秒でおこなわれる可能性がある流動性の低い資産では、サービスがフリーズする可能性はあるでしょうか。実際には完全に停止しないため、閉じられなくなる可能性はあるでしょうか。タイマーが数秒待機しているためにこのようなことが起こったのでしょうか。

これは良い質問です。なぜこれが起こらないのか見てみましょう。

                bool LoopEventOnTime(const bool bViewBuider, const bool bViewMetrics)
                        {

                                u_Interprocess Info;
                                int iPos, iTest;
                                
                                iTest = 0;
                                while ((iTest == 0) && (!_StopFlag))
                                {
                                        iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
                                        iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
                                        iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
                                        if (iTest == 0) Sleep(100);
                                }
                                if ((iTest < 0) || (_StopFlag)) return false;
                                AdjustPositionToReplay(bViewBuider);
                                m_MountBar.delay = 0;
                                while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
                                {
                                        CreateBarInReplay(bViewMetrics);
                                        iPos = (int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - 1].time_msc : 0);
                                        m_MountBar.delay += (iPos < 0 ? iPos + 1000 : iPos);
                                        if (m_MountBar.delay > 400)
                                        {
                                                if (ChartSymbol(m_IdReplay) == "") break;
                                                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                                if (!Info.s_Infos.isPlay) return true;
                                                Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                                Sleep(m_MountBar.delay - 20);
                                                m_MountBar.delay = 0;
                                        }
                                }                               
                                return (m_ReplayCount == m_Ticks.nTicks);
                        }

本当の問題は、スリープ機能に達すると、サービスがしばらく「停止」したままになるのですが、いかなる場合でもサービスを終了できないことです。実際、呼び出しによって停止フラグの状態が変更されると、STOP要求で停止できます。なぜわかるのでしょうか。これは Sleep関数のドキュメントに書かれていることです。以下は、物事を明確にするための要約です。

メモ

指標はインターフェイススレッドで実行され、速度を低下させてはいけないため、Sleep()関数はカスタム 指標から呼び出すことはできません。この関数には、0.1秒ごとにEA停止フラグのステータスを確認する機能が組み込まれています

したがって、サービスが停止しているかどうかを常に確認する必要はありません。MetaTrader 5の実装がこれをおこなってくれます。これは最高です。これにより、機能を維持し、同時にユーザーとの対話性を維持する方法の作成に関連する多くの作業が節約されます。


クイックナビゲーションシステムの修正を実装する

次に、ナビゲーションシステムの問題を解決して、すべてを元の状態に戻します。MQL5だけを使用しても解決できない小さな欠点が1つあります。また、この段階ではDLLの使用を強制するつもりはないため、MetaTrader 5プラットフォームでは細かい点で使用する必要があります。私たちは物事を正しく保つためにこれをおこなっています。実際、やるべきことは非常に単純で、ある意味では愚かなことですらあります。ただし、何がおこなわれるかを理解するには、ここで説明する内容に注意を払う必要があります。なぜなら、最初はほとんど直観的に思えるかもしれませんが、よほど注意深くなければ本当に理解できないかもしれません。

とにかく、まずコードがどのように書かれているかを見てみましょう。

#property service
#property icon "\\Images\\Market Replay\\Icon.ico"
#property copyright "Daniel Jose"
#property version   "1.18"
#property description "Replay-simulation system for MT5."
#property description "It is independent from the Market Replay."
#property description "For details see the article:"
#property link "https://www.mql5.com/ja/articles/11113"
//+------------------------------------------------------------------+
#define def_Dependence  "\\Indicators\\Market Replay.ex5"
#resource def_Dependence
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string            user00 = "Mini Dolar.txt";      //"Replay" config file.
input ENUM_TIMEFRAMES   user01 = PERIOD_M1;             //Initial timeframe for the chart.
input bool              user02 = true;                  //Visual bar construction.
input bool              user03 = true;                  //Visualize creation metrics
//+------------------------------------------------------------------+
void OnStart()
{
        C_Replay        *pReplay;

        pReplay = new C_Replay(user00);
        if ((*pReplay).ViewReplay(user01))
        {
                Print("Permission received. The replay service can now be used...");
                while ((*pReplay).LoopEventOnTime(user02, user03));
        }
        delete pReplay;
}
//+------------------------------------------------------------------+

これで、バー作製の視覚化を有効または無効にする機能を備えたシステムが再びできました。しかし、決定はユーザー次第です。必要に応じて、この視覚化をオフにすることができます。コーディングの観点からは、何の違いもありません。その理由は、ティックチャートで気配値表示を使用したい場合は、何らかの形でMetaTrader 5で何かをおこなう必要があるからです。これは、チャートに適切な値が含まれていることを確認するために必要です。ただし、通常のチャートと価格ラインの場合は、正しく構成されているため、変更や介入は必要ありません(このようにしたとき、私はそう思ったのですが、後でそれが間違っていたことがわかります。修正方法がよくわからないバグがありますが、それについては別の記事で説明します)。

これをおこなうには、C_Replayクラスにいくつかの変更を加える必要がありました。最初の変更はバー作成ルーチンに関するものでした。以下のコードを参照してください。

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

                                bool bNew;
                                MqlTick tick[1];

                                if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
                                {                               
                                        if (bViewMetrics) Metrics();
                                        m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                bNew = (def_Rate.tick_volume == 0);
                                def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : 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);
                                tick = m_Ticks.Info[m_ReplayCount];
                                if (bViewTicks) CustomTicksAdd(def_SymbolReplay, tick);
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

このルーチンは、新しい引数を追加するために必要でした。この引数は、気配値表示ウィンドウへのティックの送信を有効または無効にします。なぜこれをおこなうのでしょうか。これは、気配値表示システムのティックチャートと棒チャートを同時に更新することができないためです。これは、サービスをいつでも開始できるように構成した場合に発生します。しかし、通常の使用では、問題なく気配値表示ウィンドウと棒グラフの両方にティックを送信できます。これは非常に奇妙なことです。

次のようにお考えかもしれません。気配値表示ウィンドウの内容を実際に変更することはできません。可能ですが、いかなる場合でも不可能です。私たちが実際にできること、そしておこなうことは古いティックを取り除くことだけです。しかし、少なくとも MetaTrader 5プラットフォームの開発者が気配値表示ウィンドウでのカスタム銘柄の使用に関連する問題を修正するまでは、こレを行うのは困難です。これは、カスタム銘柄に配置されたティックが、そのようなカスタムティックを表示できるウィンドウから消えないためです。奇妙なことに、それらはそこに残っているため、前の位置に戻ると理解するのが難しくなります。

いずれの場合も、ポジションシステムの管理を担当する関数を以下に示します。

                void AdjustPositionToReplay(const bool bViewBuider)
                        {
                                u_Interprocess Info;
                                MqlRates       Rate[def_BarsDiary];
                                int            iPos, nCount;
                                
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                if (m_ReplayCount == 0)
                                        for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++);
                                if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return;
                                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);
                                        CustomTicksDelete(def_SymbolReplay, m_Ticks.Info[iPos].time_msc, LONG_MAX);
                                        if ((m_dtPrevLoading == 0) && (iPos == 0)) FirstBarNULL(); 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);) CreateBarInReplay(false, false);
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                Info.s_Infos.isWait = false;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                        }

以前のバージョンと比較した唯一の変更点は、単に気配値表示ウィンドウのティックチャート上の特定のポイントからティックを削除する関数です。なぜ今までこの実装を見なかったのでしょうか。なぜなら、私はまだ両方のチャート(棒グラフとティックグラフ)で動的なデータ更新を実現しようとしていたからです。しかし、更新システムでエラーや問題が発生せずには実行できませんでした。ある時点で、単純に棒グラフのみを更新すると決めたので、この関数には2つのパラメータが含まれるようになりました。 

システムはティックチャート表示と気配値表示ウィンドウの使用を実装する前とほぼ同じになったので、この記事を終える前にもう1つ説明します。しかし、実際のデータを扱うときにリプレイ/モデリングがどのように機能するかを理解していただければ幸いです。ここで、データがシミュレートされた場合にのみ、気配値ウィンドウにティックを追加します。そして、これがまさに次のセクションのトピックです。


気配値表示でのシミュレートされたデータの使用

相場監視ウィンドウのティックに関するこのティックのトピックを別の記事に拡張したくないので、その方法を見てみましょう。より正確には、この種の状況に対する私の提案を見てみましょう。1分足のティックのモデリングに関しては、ここでの質問はこれまでにおこなわれたものよりもはるかに単純です。これまでの説明をすべて理解していれば、問題なく理解できるでしょう。

実際の取引データを使用する場合とは異なり、シミュレートされたデータを使用する場合、最初はいくつかの種類の情報が不足します。そのデータを作成することが不可能というわけではありませんが、これは慎重におこなう必要があります。最後の価格がBIDとASKの間の領域を超えたときの情報のことです。リプレイをよく見ると、これが起こった瞬間に、BIDとASKによって制限された領域の突破が常に非常に速く、まれであることがわかります。実際、私の市場での経験では、価格変動が急増したときにこれらのことが起こります。ただし、先ほども述べたように、これらはまれな出来事です。

メモ:したがって、スプレッドの範囲内で常に作業できる、そして今後も作業できるとは決して信じないでください。場合によっては、システムがスプレッドを超えることもあります。注文システムを開発する際には、この情報とその適切な理解が重要となるため、このことを理解しておくことが重要です。 

重要な事実:価格はBIDとASKで構成されていますが、これはシステムにギャップや崩壊があることを意味するものではありません。新しいBIDとASK値を含む取引サーバーの更新を時間内に受信できなかっただけです。しかし、オーダーブックに従ってみると、多くの人が想像しているものとは少し異なることがわかります。したがって、存在する問題を本当に理解するには、取引システム全体について多くの経験を積んでいる必要があります。

これを理解すれば、そのような動きをシミュレーションシステムに組み込むことを検討することもできます。これにより、状況はさらに現実的になります。しかし、そのようなものは慎重に扱わなければならないことを覚えておいてください。理想的には、このタイプの動きがシミュレートされる資産をよく知っている必要があります。この方法によってのみ、起こっていることを実際の市場で実際に起こることに近づけることができます。シミュレーターでこのタイプの動きを有効にする方法を理解するために、まず、価格が常に BIDからASKの範囲内に収まるようにシミュレーターを実装する方法を見てみましょう。

まず、新しい変数を追加する必要があります。

struct st00
        {
                MqlTick  Info[];
                MqlRates Rate[];
                int      nTicks,
                         nRate;
                bool     bTickReal;
        }m_Ticks;

これを使用して、ティックが実際のデータに基づいているか、シミュレートされたデータに基づいているかを確認します。実際には BIDとASKの動きをシミュレートするわけではないため、この違いは重要です。これらの制限は、シミュレーターによって生成された最後の取引価格の値に基づいて構築されます。しかし、主な理由は、BIDとASKの値が報告される取引量に含まれていないことです。シミュレーション機能をシンプルにするために、BIDとASKを別の場所で生成するようにこのセットアップを作成します。

新しい変数を取得したら、それを正しく初期化する必要があります。初期化される場所は2か所あります。1つ目は、シミュレートされたティックを使用していることを示す場合です。

                bool BarsToTicks(const string szFileNameCSV)
                        {
                                C_FileBars      *pFileBars;
                                int             iMem = m_Ticks.nTicks;
                                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)) Simulation(rate[0], local);
                                ArrayFree(local);
                                delete pFileBars;
                                m_Ticks.bTickReal = false;
                                
                                return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
                        }

この変数を初期化するもう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;
                                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);
                                }
                                m_Ticks.bTickReal = true;
                                                                        
                                return dtRet;
                        };

これで、実際のティックを扱っているのか、それともシミュレートされたティックを扱っているのかがわかったので、作業に取り掛かることができます。ただし、C_Replayクラスに進んで何かの構成を開始する前に、シミュレーター自体にいくつかの小さな変更を加える必要があります。実際のティックを読み込むときは、ミリ秒フィールドの値が特定の時点を表すように修正されるように時間を調整します。ただし、シミュレーターはまだこの調整をおこなっていません。したがって、C_Replayクラスを変更した後でシステムを実行しようとしても、シミュレートされたデータを実際に把握することはできません。これは、ミリ秒フィールドで表される時間が正しくないためです。

この問題を解決するために、次の変更を加えます。

inline void Simulation(const MqlRates &rate, MqlTick &tick[])
                        {
#define macroRandomLimits(A, B) (int)(MathMin(A, B) + (((rand() & 32767) / 32767.0) * MathAbs(B - A)))

                                long     il0, max, i0, i1;
                                bool     b1 = ((rand() & 1) == 1);
                                double   v0, v1;
                                MqlRates rLocal;
                                
                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                max = rate.tick_volume - 1;     
                                v0 = 4.0;
                                v1 = (60000 - v0) / (max + 1.0);
                                for (int c0 = 0; c0 <= max; c0++, v0 += v1)
                                {
                                        tick[c0].last = 0;
                                        tick[c0].flags = 0;
                                        il0 = (long)v0;
                                        tick[c0].time = rate.time + (datetime) (il0 / 1000);
                                        tick[c0].time_msc = (tick[c0].time * 1000) + (il0 % 1000);
                                        tick[c0].time_msc = il0 % 1000;
                                        tick[c0].volume_real = 1.0;
                                }
                                tick[0].last = rate.open;
                                tick[max].last = rate.close;
                                for (int c0 = (int)(rate.real_volume - rate.tick_volume); c0 > 0; c0--)
                                        tick[macroRandomLimits(0, max)].volume_real += 1.0;                                     
                                i0 = (long)(MathMin(max / 3.0, max * 0.2));
                                i1 = max - i0;
                                rLocal = rate;  
                                rLocal.open = rate.open;
                                rLocal.close = (b1 ? rate.high : rate.low);
                                i0 = RandomWalk(1, i0, rLocal, tick, 0);
                                rLocal.open = tick[i0].last;
                                rLocal.close = (b1 ? rate.low : rate.high);
                                RandomWalk(i0, i1, rLocal, tick, 1);
                                rLocal.open = tick[i1].last;
                                rLocal.close = rate.close;
                                RandomWalk(i1, max, rLocal, tick, 2);
                                for (int c0 = 0; c0 <= max; c0++)
                                {
                                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                                        m_Ticks.Info[m_Ticks.nTicks++] = tick[c0];
                                }
#undef macroRandomLimits
                        }

一部のコードを削除し、推奨されるコードに置き換えます。このようにして、ミリ秒単位の時間は C_Replayクラスが期待するものと互換性があります。ここで、シミュレートされたコンテンツを表示するために変更を加えることができます。

C_Replayクラスでは、次のコードに示されている1つの関数に変更を加えることに焦点を当てます。

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

                                bool bNew;
                                MqlTick tick[1];

                                if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
                                {                               
                                        if (bViewMetrics) Metrics();
                                        m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                bNew = (def_Rate.tick_volume == 0);
                                def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : 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)
                                        {
                                                tick[0].bid = tick[0].last - m_PointsPerTick;
                                                tick[0].ask = tick[0].last + m_PointsPerTick;
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

ここには非常に簡単な変更があります。この変更は、BID値とASK値の作成と表示のみを目的としています。ただし、この作成は最後の取引価格の値に基づいており、シミュレーション値を扱う場合にのみ実行されることにご注意ください。このコードを実行すると、EXCELを使用したときと同じように、ランダムウォークシステムによって生成されたグラフの内部表現が取得されます。記事の中で「リプレイシステムの開発 - 市場シミュレーション(第15回):シミュレーターの誕生(V) - ランダムウォーク」で、同じ視覚化をおこなう別の方法があると述べましたが、私はこのモデルを参照していました。しかし、そのときはそれをどうするかについていうときではありませんが、今がその時間です。

これらのBID値とASK値が作成されていない場合は、最後にシミュレートされた価格に基づいて値を表示することしかできません。これで十分かもしれませんが、BIDとASKの値を確認するのが好きな人もいます。ただし、この方法でデータを使用することは完全に適切というわけではありません。実際に触ることなく、BIDやASK内で正確に操作がおこなわれるということは、市場で直接操作がおこなわれていることを示しています。したがって、取引はオーダーブックなしで実行されます。この場合、シミュレーターで見るように、価格は変動しないはずですが、価格は変動します。よって、緑色で強調表示されている部分のみを修正する必要があります。これは、動きが少なくとも実際に期待されるものと一致するようにおこなわれます。

強調表示されたセグメントと以下に示す変更を確認してください。

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - m_PointsPerTick;
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + m_PointsPerTick;
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                                tick[0].ask = tick[0].last + m_PointsPerTick;
                                                tick[0].bid = tick[0].last - m_PointsPerTick;
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

いくつかのコードを削除し、さらにいくつかのポイントを追加しました。BIDとASKの値は静的であり、これは小さな指標を構築するために必要であることにご注意ください。非常にシンプルですが、革命を引き起こすには十分です。システムの起動時にはこれらの値がゼロになる可能性が非常に高いため、最初にASKに重点が置かれる呼び出しがおこなわれ、わずか1ティックのかなり狭いチャネルが設定されます。その後、最後の取引価格がこのチャネルを離れるまで、そのチャネルに残ります。

これはシンプルでありながら機能的です。BID値がASK値と衝突しないようにご注意ください(これは為替市場での話です。外国為替市場では話が異なりますが、これについては後で説明します)。衝突の原因は、最後に実行された取引の価値です。上記のコードに少し変更を加えたらどうなるでしょうか?非常に微妙なものです。実際に最後の取引価格を変更せずにBIDまたはASK値が変更された場合はどうなるでしょうか。

コードを次のように変更してみましょう。

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

面白いですね。システムに一定レベルの無作為性を追加することで、直接注文を含めることが可能になりました。つまり、BIDまたはASKを変更せずに約定される注文のことです。実際の市場では、このような注文はそれほど頻繁に発生するものではなく、システムが表示する形式ではありません。しかし、この事実を無視すると、BIDとASKの間に小さなスプレッドが含まれる場合があるという優れたシステムがすでに構築されていることになります。言い換えれば、シミュレータは、実際の市場におけるより一般的な状況に実際に適応します。ただし、過剰な直接注文には注意が必要です。この過剰を避けるために、物事をもう少し無作為にしないようにすることができます。

これをおこなうには、最後の変更を実装する必要があります。以下をご覧ください。

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                double  dSpread;
                                                int     iRand = rand();
                                                
                                                dSpread = m_PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_PointsPerTick : 0 ) : 0 );
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - dSpread;
                                                        BID = tick[0].bid = tick[0].last - (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                        ASK = tick[0].ask = tick[0].last + dSpread;
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

ここでは、無作為生成の複雑さのレベルを制御します。これは、すべてをある程度の不測の事態の範囲内に保つためにおこなわれます。直接注文も随時おこないますが、これはより制御された量になるでしょう。それには、ここでこれらの値を調整するだけです。それらを調整することで、スプレッドが可能な最小値よりわずかに大きくなる可能性がある小さなウィンドウを作成します。その結果、以前の記事や今までは不可能だった、モデリングシステムによって生成される直接の注文にも随時対応していきます。


結論

この記事では、気配値表示を使用してチャート上にティックを設定および作成するシステムを実装する方法を説明しました。これは前回の記事で開始しました。結果的に、直接注文でもシミュレーションできるシミュレーションシステムを構築しました。これは私たちの当初の目標には含まれていませんでした。これが一部の種類の取引システムで完全に使用できるようになるまでには、まだかなり長い道のりがあります。今日おこなったことはほんの始まりにすぎません。

次回の記事では、市場リプレイ/シミュレーションシステム作成の連載を続けます。添付ファイルには、システム動作をテストおよび確認するための4つの異なるリソースが含まれています。シミュレートされた値と実際の値の違いを確認できるように、実際のティックデータと1分足の両方を提供することにご注意ください。これにより、物事をより深く分析できるようになります。ここで説明したことをすべて理解するには、両方のモードでリプレイ/シミュレーションサービスを実行する必要があります。まずシミュレーションを実行するときにカスタム銘柄を確認アウトし、次にリプレイでその内容を確認します。ただし、チャート自体ではなく、ティックウィンドウに注意を向けてください。違いが本当に顕著であることがわかります。少なくとも気配値表示ウィンドウの内容に関してはですが。


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

添付されたファイル |
Market_Replay_rvt_18.zip (12899.62 KB)
リプレイシステムの開発 - 市場シミュレーション(第19回):必要な調整 リプレイシステムの開発 - 市場シミュレーション(第19回):必要な調整
ここでは、コードに新しい関数を追加する必要がある場合に、スムーズかつ簡単に追加できるように基礎を整えます。現在のコードでは、有意義な進歩を遂げるために必要な事柄の一部をまだカバーまたは処理できません。最小限の労力で特定のことを実装できるようにするには、すべてを構造化する必要があります。すべてを正しくおこなえば、対処が必要なあらゆる状況に非常に簡単に適応できる、真に普遍的なシステムを得ることができます。
ニューラルネットワークが簡単に(第53回):報酬の分解 ニューラルネットワークが簡単に(第53回):報酬の分解
報酬関数を正しく選択することの重要性については、すでに何度かお話ししました。報酬関数は、個々の行動に報酬またはペナルティを追加することでエージェントの望ましい行動を刺激するために使用されます。しかし、エージェントによる信号の解読については未解決のままです。この記事では、訓練されたエージェントに個々のシグナルを送信するという観点からの報酬分解について説明します。
リプレイシステムの開発 - 市場シミュレーション(第20回):FOREX (I) リプレイシステムの開発 - 市場シミュレーション(第20回):FOREX (I)
この記事の最初の目的は、外国為替取引のすべての可能性をカバーすることではなく、少なくとも1つのマーケットリプレイを実行できるようにシステムを適応させることです。シミュレーションはまた別の機会にしますが、ティックがなくバーだけでも、少しの努力で外国為替市場で起こりうる取引をシミュレートすることができます。シミュレーターをどのように適応させるかを検討するまでは、この状態が続くでしょう。システム内部でFXのデータに手を加えずに作業しようとすると、さまざまなエラーが発生します。
ニューラルネットワークが簡単に(第52回):楽観論と分布補正の研究 ニューラルネットワークが簡単に(第52回):楽観論と分布補正の研究
経験再現バッファに基づいてモデルが訓練されるにつれて、現在のActor方策は保存されている例からどんどん離れていき、モデル全体としての訓練効率が低下します。今回は、強化学習アルゴリズムにおけるサンプルの利用効率を向上させるアルゴリズムについて見ていきます。