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

リプレイシステムの開発 - 市場シミュレーション(第9回):カスタムイベント

MetaTrader 5 | 19 12月 2023, 08:18
171 0
Daniel Jose
Daniel Jose

はじめに

前回の記事、リプレイシステムの開発 - 市場シミュレーション(第8回):指標のロックでは、コントロール指標をロックする方法を紹介しました。この目標は達成することができましたが、まだ取り組むべき点があります。よくご覧になれば、リプレイ/シミュレーションの開始点を変えるたびに、取引バーが構築される様子が素早く表示されることにお気づきでしょう。ある意味、これはそれほど問題ではないし、ある人にとっては面白いかもしれませんが、他の人にとってはそうでもありません。これからはギリシャ人とトロイ人を喜ばせるように努力します。最適に機能するように、リプレイ/シミュレーター サービスを実装する方法を見てみましょう。言い換えれば、バーが作られるのを見ることができるかどうかです。


ギリシャ人とトロイ人を喜ばす

最初のステップは、サービスファイルに新しい変数またはパラメータを追加することです。

input string            user00 = "Config.txt";  //Replay configuration file
input ENUM_TIMEFRAMES   user01 = PERIOD_M5;     //Starting time
input bool              user02 = true;          //Visualize construction of bars



ここで、ユーザーに決断をさせるプロセスを開始します。前にも言ったように、バーが作られるのを見て楽しむ人もいれば、気にしない人もいます。

このステップが完了したら、次のステップでこのパラメータをC_Replayクラスに渡します。

while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value)) && (!_StopFlag))
{
        if (!Info.s_Infos.isPlay)
        {
                if (!bTest) bTest = true;
        }else
        {
                if (bTest)
                {
                        delay = ((delay = Replay.AdjustPositionReplay(user02)) >= 0 ? 3 : delay);
                        bTest = false;
                        t1 = GetTickCount64();
                }else if ((GetTickCount64() - t1) >= (uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < 0) break;
                        t1 = GetTickCount64();
                }
        }
}



これでC_Replayクラスに進み、作業を開始することができます。このタスクは表面的には単純に見えますが、障害や課題を含んでいます。これまで、市場リプレイデータは取引されたティックに基づいており、チャートは1分足バーでプロットされていました。単にバーを追加したり削除したりするだけではありません。様々な要素が均一になるように扱わなければなりません。簡単なことではないでしょうが、問題を解くのは好きだし、この問題もなかなか面白そうです。

最初のステップは、取引されたティックのファイルを読み込んでいる間に1分足バーを作成することですが、考慮する必要がある別の側面があります。細心の注意を払うべきです。この課題にアプローチする方法は、以下の通りです。最初から、システムに新しい変数のセットを導入します。

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



このセットには1分足が含まれ、ティックファイルの読み込みと同時にプロットします。ここまでのコードを見て、C_ReplayクラスにあるEvent_OnTime関数が、取引されたティックの値に基づいて1分足のバーをプロットする機能を持っていることに気づくでしょう。しかし、この関数を呼び出してこのタスクを実行させることはできません。実際、これは慎重におこなうことができます。プロセスの最後に、リプレイサービスで作成されたバーをすべて削除すればいいのです。こうすることで、システム使用の準備が整います。ただし、Event_OnTimeが機能する方法では、各呼び出しにわずかな遅延が生じる一方、取引されたティックに関連する呼び出しの数は、通常、比較的大きくなります。少し違ったアプローチを取らなければなりません。

すでに述べたように、少し違ったアプローチを探さなければなりません。こうして、次のような関数ができました。

inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick)
                {
                        if (rate.time != macroRemoveSec(tick.time))
                        {
                                rate.real_volume = (long) tick.volume_real;
                                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;
        
                        return false;
                }



ここでやっていることは、Event_OnTimeがやっていることと本質的に同じですが、これをティックごとにおこなっていきます。以下、簡単に説明しましょう。ティックに表示された時刻とバーに記録された時刻が異なる場合、最初のバーが構築されます。trueを返すことで、呼び出し元に新しいバーが作成されることを伝え、必要な変更をおこなえるようにします。それ以降の呼び出しでは、それに応じて値を調整します。この場合、新しいバーが作成されなかったことを示すためにfalseを返します。関数自体は非常にシンプルですが、使用する際には注意が必要です。

まず、配列を正しく初期化してください。これがどこでおこなわれているかを見てみましょう。

bool SetSymbolReplay(const string szFileConfig)
{
        int     file;
        string  szInfo;
        bool    isBars = true;
                                
        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
                MessageBox("Failed to load the\nconfiguration file.", "Market Replay", MB_OK);
                return false;
        }
        Print("Loading data for replay. Please wait....");
        ArrayResize(m_Ticks.Rate, 540);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        while ((!FileIsEnding(file)) && (!_StopFlag))
        {
                szInfo = FileReadString(file);
                StringToUpper(szInfo);
                if (szInfo == def_STR_FilesBar) isBars = true; else
                if (szInfo == def_STR_FilesTicks) isBars = false; else
                if (szInfo != "") if (!(isBars ? LoadPrevBars(szInfo) : LoadTicksReplay(szInfo)))
                {
                        if (!_StopFlag)
                                MessageBox(StringFormat("File %s from%s\ncould not be loaded.", szInfo, (isBars ? def_STR_FilesBar : def_STR_FilesTicks), "Market Replay", MB_OK));
                        FileClose(file);
                        return false;
                }
        }
        FileClose(file);
        return (!_StopFlag);
}



これを事前に正しくおこなわないと、バー作成関数を正しく使うことができません。そこで次の疑問が生じます。なぜ最初の配列のインデックスに値-1を指定するのでしょうか。0がスタート値ではないのでしょうか。そう、0なのですが、最初の呼び出しは-1から始めます。これは常にtrueになります。0から始めるとすると、バー作成を呼び出した直後に追加のテストを実行しなければなりませんが、-1に設定すると、この追加チェックは不要になります。配列を540ポジションで初期化することに注意することが重要です。これは、ブラジル証券取引所(B3)の通常の取引日に通常存在する1分足の数に相当します。

このステップが完了したら、取引されたティックの読み取りに移ることができます。

bool LoadTicksReplay(const string szFileNameCSV)
{
        int     file,
                old;
        string  szInfo = "";
        MqlTick tick;
        MqlRates rate;
                                
        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, 540, 540);
                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 file a traded ticks.");
                        return false;
                }
                Print("Loading replay ticks. 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 = macroRemoveSec(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 = m_Ticks.nTicks;
                                        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 many data in the tick file.\nCannot continue...");
                        return false;
                }
        }else
        {
                Print("Tick file ", szFileNameCSV,".csv not found...");
                return false;
        }
        return (!_StopFlag);
};



ここでひとつ重要なことがあります。分足バーの数がここで指定した数より多い場合、初期値と予備値を調整する必要があるということです。この値は、9:00から18:00までの取引時間(540分)に適していますが、取引時間が長くなる場合は、あらかじめこの値を大きくしておく必要があります。ただし、考慮すべき時間帯は、取引セッションの開始時間と終了時間であることに注意することが重要です。これは、バーファイルではなく、取引されたティックファイルを指します。ティックファイルに基づいてバーが生成され、このセッションが特定のファイルで異なる場合、実行中(RUN TIME)に問題が発生する可能性があるためです。B3取引所の取引時間は通常540分なので、この値を使用します。

これで、取引されたティックのファイルを提示することができます。こうして一度に1ティックを捕捉し、1分足バーを構築します。ただし、次の点に注意することが重要です。バーは、ある程度の取引量がある場合にのみ生成されます。それ以外の場合、ティックは資産の呼び値(売り/買い)に対する調整を表すため、考慮されないということです。注意:近い将来、このシステムを外国為替市場に適応させる予定なので、そのような状況に対処するつもりですが、今は省略しましょう。

リプレイ/シミュレーションではスプレッドの値を使用しないため、より有意義な目的で使用されます。ただし、これはスプレッド値ではないことにご注意ください。したがって、正しいスプレッドの値を必要とする指標がある場合は、別のアプローチを使用する必要があります。スプレッドを格納する変数には、カウンタの位置の値を格納することができます。これは近い将来、非常に役に立つことが証明されるでしょう。

これですべてが正しく設定されたので、1分足バーのデータを保存して次のステップに進むことができます。読み取りシステムに他の変更が加えられていないためです。したがって、読む順番についてこれ以上コメントする必要はありません。

次にメイン関数を見てみましょう。

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


この関数は完全には完成しておらず、今後も変化し続けるでしょうが、今後の説明の混乱を避けるため、この記事の作成中に追加および削除されたものの概要を説明します。こうすることで、読者は何が起こっているのかをより理解しやすくなり、もし変更を加えたい場合にも、それが容易になります。これらの記事を読み返し、各論点で実際に何が起こっているのかを見直す必要があります。ここに書かれていないことは、すでに今までの記事でカバーされています。

最初のステップは、関数内部の時間位置を設定するローカル変数を宣言することです。この設定にしておけば、前に進んでから少し戻ろうと思っても、リプレイを最初からやり直す必要はありません。その点はすぐに分かります。現在位置が前進すべきか後退すべきかを判断するためにいくつかの計算をした後、最初に取るべき行動を見つけます。ポジションを戻す必要がある場合、この2行はアクションの開始時にリプレイ/シミュレーションを開始します。ただし、その必要はないかもしれません。もしあなたやユーザーが、バーが作られるのを見たくないという意思を示した場合、短いループに入り、取引されたティックを読むときに記録されたすべての1分足のバーの内容を確認します。 

さて、現時点ではあまり明確ではないかもしれません問題があります。取引されたティックを1分足バーに変換すると、カウンタの相対位置が得られ、同時に新しいバーの開始時間が得られます。この情報は便利で必要なもので、指定された時間以降に表示されるすべてのバーを消去することができます。カウンタ値がユーザーが要求した新しい相対位置決め値と同一である可能性は低くなります。システムはポジションを一致させるためにわずかな調整をおこないますが、この調整は非常に素早くおこなわれます。そのため、バーの作成はほとんど目立たなくなります。

ただし、先に述べたように、この機能はまだ完全ではありません。説明した操作は、ユーザーが現在のカウンタの位置から後退を行った場合にのみ使用されます。カウンタの位置から前進した場合、バーが作られます。ギリシャ人もトロイ人も、すべての人を喜ばせたいので、この小さな間違いを修正して、バーの作成が事前に見えないようにする必要があります。それほど複雑ではありません。前進システムを含まない上のコードと、それを含む下のコードを比較してみましょう。

int AdjustPositionReplay(const bool bViewBuider)
{
#define macroSearchPosition     {                                                                                               \
                dt_Local = m_dtPrevLoading; m_ReplayCount = count = 0;                                                          \
                if (!bViewBuider) for (count = 1; (count < m_Ticks.nRate) && (m_Ticks.Rate[count - 1].spread < iPos); count++)  \
                        { dt_Local = m_Ticks.Rate[count].time;  m_ReplayCount = m_Ticks.Rate[count - 1].spread; }               \
                                }

        u_Interprocess  Info;
        MqlRates        Rate[def_BarsDiary];
        int             iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks),
                        count;
        datetime        dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
        if (iPos < m_ReplayCount)
        {
                macroSearchPosition;
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }if ((iPos > m_ReplayCount) && (!bViewBuider))
        {
                macroSearchPosition;                    
                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, count);
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        return Event_OnTime();
}



違いがお分かりになったでしょうか。マクロについて考えているのなら、それは忘れてください。マクロは、同じコードを2つの異なる場所で繰り返す手間を省くためだけにあるのです。実際、ほとんど違いはありません。おそらく唯一違うのは、追加のバーを追加するラインでしょう。リプレイシステムを適用してみると、前進と後退のポイントが、あるバーの終値と次のバーの始値とが一致する可能性が低いことに気づくでしょう。これは、この線に対応する余りが常に存在するからです。しかし、このセッティングはスピードが速いため、この改良に気づくことはほとんどないでしょう。


ユーザーへの警告

私たちのリプレイシステムは、これまで必要でなかったいくつかの追加要素を取り入れ始めるべき段階にあります。そのひとつが、リプレイをシミュレートまたは継続するためのデータがシステム内になくなった場合にユーザーに通知する機能です。この警告がないと、ユーザーは単にシステムがクラッシュしたか、何らかの異常事態が発生したと思い込むかもしれません。そのような思い込みを防ぐために、まずはいくつかの追加情報を加えましょう。最初のステップは、使用できるデータがもうないという警告です。その方法を理解するために、以下のコードを見てみましょう。

void OnStart()
{
        ulong t1;
        int delay = 3;
        long id = 0;
        u_Interprocess Info;
        bool bTest = false;
        
        Replay.InitSymbolReplay();
        if (!Replay.SetSymbolReplay(user00))
        {
                Finish();
                return;
        }
        Print("Wait for permission from [Market Replay] indicator to start replay ...");
        id = Replay.ViewReplay(user01);
        while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(id) != "")) Sleep(750);
        if ((_StopFlag) || (ChartSymbol(id) == ""))
        {
                Finish();
                return;
        }
        Print("Permission granted. The replay service can now be used...");
        t1 = GetTickCount64();
        while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value)) && (!_StopFlag))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest) bTest = true;
                }else
                {
                        if (bTest)
                        {
                                if ((delay = Replay.AdjustPositionReplay(user02)) < 0) AlertToUser(); else
                                {
                                        delay = (delay >= 0 ? 3 : delay);
                                        bTest = false;
                                        t1 = GetTickCount64();
                                }                               
                        }else if ((GetTickCount64() - t1) >= (uint)(delay))
                        {
                                if ((delay = Replay.Event_OnTime()) < 0) AlertToUser();
                                t1 = GetTickCount64();
                        }
                }
        }
        Finish();
}
//+------------------------------------------------------------------+
void AlertToUser(void)
{
        u_Interprocess Info;
        
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        Info.s_Infos.isPlay = false;
        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        MessageBox("No more data to use in replay-simulation", "Service Replay", MB_OK);
}
//+------------------------------------------------------------------+
void Finish(void)
{
        Replay.CloseReplay();
        Print("The replay service completed...");
}
//+------------------------------------------------------------------+



このような警告を出す場合、2つのケースがあります。最初のものは、通常のリプレイ実行中に発生するもので、これは最も一般的なケースです。しかし、もう一つの選択肢があります。ユーザーがスクロールバーの端に位置を合わせた場合です。

int AdjustPositionReplay(const bool bViewBuider)
{

// ... Code ...

        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));

// ...  Rest of the code ...



それにもかかわらず、答えはいつも同じです。グローバルターミナル変数に含まれる値を取り出し、それを使って一時停止モードであることを示します。そしてもう一度記録し、何が起こったかを報告するウィンドウを表示します。基本的にはこのようなことをすることになりますが、非常に役に立つでしょう。そうすれば、かわいそうなユーザーは何が起こったのかを知ることができます。


「お待ちください」警告の追加

さて、リプレイシステムは、ユーザーがバーの構築過程を見たいかどうかを指示できるようになりましたが、実際にユーザーがバーの構築過程をモニターしたい場合、ちょっとした問題があります。これがこのトピックを立てた理由です。

リプレイサービスが正しい位置に到達するのを待っている間に、バーが作られていくのを見たいとき、私たちはいつでも進行を止めたり始めたりできる印象を受けます。再生ボタンと一時停止ボタンがあるからです。しかし、リプレイサービスがシステムをリリースする正しい位置に到達するまでは、そのどちらも実際におこなうことはできません。このような状況では、何が起こっているのかよくわからず、少し混乱してしまうものです。しかし、この提示されたボタンを、待つ必要性を示す別のボタンに置き換えれば、状況は一変します。そう思いませんか。

しかし、単にボタンを追加するだけでは十分ではありません。コントロール指標に何を表示すべきか、あるいは何を表示すべきでないかをサービスが指示できるようにするために、いくつかの追加ステップを実行する必要があります。InterProcess.mqhヘッダーファイルに新しい変数を追加することから始めましょう。

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_GlobalVariableReplay        "Replay Infos"
#define def_GlobalVariableIdGraphics    "Replay ID"
#define def_SymbolReplay                "RePlay"
#define def_MaxPosSlider                400
#define def_ShortName                   "Market Replay"
//+------------------------------------------------------------------+
union u_Interprocess
{
        union u_0
        {
                double  df_Value;       // The value of the terminal's global variable...
                ulong   IdGraphic;      // Contains the asset chart ID....
        }u_Value;
        struct st_0
        {
                bool    isPlay;         // Specifies if we are in the play or pause mode ...
                bool    isWait;         // Asks the user to wait...
                ushort  iPosShift;      // A value between 0 and 400 ...
        }s_Infos;
};
//+------------------------------------------------------------------+



この値は、サービスと指標間で転送され、他のコントロールよりも優先されます。従って、表示する必要がある場合、コントロール指標は他のことができなくなります。変数を定義したので、次はリプレイサービスに行き、コントロール指標と通信するために必要なコードを追加する必要があります。そのためには、C_Replayクラスにいくつかのコードを追加する必要があります。それほど難しいことではありません。

int AdjustPositionReplay(const bool bViewBuider)
{
#define macroSearchPosition     {                                                                                               \
                dt_Local = m_dtPrevLoading; m_ReplayCount = count = 0;                                                          \
                if (!bViewBuider) for (count = 1; (count < m_Ticks.nRate) && (m_Ticks.Rate[count - 1].spread < iPos); count++)  \
                        { dt_Local = m_Ticks.Rate[count].time;  m_ReplayCount = m_Ticks.Rate[count - 1].spread; }               \
                                }

        u_Interprocess  Info;
        MqlRates        Rate[def_BarsDiary];
        int             iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks),
                        count;
        datetime        dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
        if (iPos < m_ReplayCount)
        {
                macroSearchPosition;
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }if ((iPos > m_ReplayCount) && (!bViewBuider))
        {
                macroSearchPosition;                    
                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, count);
        }
        if (bViewBuider)
        {
                Info.s_Infos.isWait = true;
                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) 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();
}



この時点には通常到達せず、何かが本当に必要な瞬間にのみ発生します。ユーザーがチャートに表示されたバーを視覚化したい場合、指標がサービスをしばらくの間利用できないことを示すようにシグナルを送信します。指標がこの値を解釈できるように、グローバルターミナル変数に記録します。そのサービスは、実際に意図されたタスクを実行します。この直後、指標を完全に無条件でリリースします。

この後、コントロール指標のコードに移り、何が起こっているかを分析することができます。ここで物事を動かすには多くのコードが必要だと思う人もいるかもしれません。しかし、ご覧の通り、最小限のコードですべての作業をおこないます。物事を単純化するために、少し抽象化するのはどうでしょうか。そのために、まずC_Control.mqhヘッダーファイルに以下の行を追加します。

enum EventCustom {Ev_WAIT_ON, Ev_WAIT_OFF};



実は、次にやることを単純化するために、抽象化のレイヤーを追加しているのです。使用する画像については、以下のスニペットに追加されていることをお忘れなく。

#define def_ButtonPlay  "Images\\Market Replay\\Play.bmp"
#define def_ButtonPause "Images\\Market Replay\\Pause.bmp"
#define def_ButtonLeft  "Images\\Market Replay\\Left.bmp"
#define def_ButtonRight "Images\\Market Replay\\Right.bmp"
#define def_ButtonPin   "Images\\Market Replay\\Pin.bmp"
#define def_ButtonWait  "Images\\Market Replay\\Wait.bmp"
#resource "\\" + def_ButtonPlay
#resource "\\" + def_ButtonPause
#resource "\\" + def_ButtonLeft
#resource "\\" + def_ButtonRight
#resource "\\" + def_ButtonPin
#resource "\\" + def_ButtonWait



ここで画像を使うことで、物事が本当にシンプルになります。サービスが実行中であることをユーザーに示すだけであり、この操作の間は他のリクエストには応答できないことを忘れないでください。

次にクラスファイルに、内部アクションを制御するためのprivate内部変数を追加します。 

class C_Controls
{
        private :
//+------------------------------------------------------------------+
                string  m_szBtnPlay;
                long    m_id;
                bool    m_bWait;
                struct st_00
                {
                        string  szBtnLeft,
                                szBtnRight,
                                szBtnPin,
                                szBarSlider;
                        int     posPinSlider,
                                posY;
                }m_Slider;
//+------------------------------------------------------------------+



この変数を追加することで、リプレイ/シミュレーションサービスの状態をすでに把握することができます。しかし、適切な場所で初期化する必要があり、最良の選択肢はクラスのコンストラクタです。

C_Controls() : m_id(0), m_bWait(false)
        {
                m_szBtnPlay             = NULL;
                m_Slider.szBarSlider    = NULL;
                m_Slider.szBtnPin       = NULL;
                m_Slider.szBtnLeft      = NULL;
                m_Slider.szBtnRight     = NULL;
        }



リプレイ/シミュレーションサービスは常に自由に起動し、どのようなコマンドにも応答することができるため、この値をfalseで初期化する必要があることに注意してください。初期化はここでおこなわれますが、他の呼び出しで正しい状態を保つようにします。しかし、今のところはこれで十分でしょう。

次に、どのイベントを本当にロックしたいのかを分析します。再生位置を前後に動かすたびに、ボタンが[再生]から[一時停止]に変わるのが見えるので、ユーザーがそのボタンにアクセスするのをブロックしたいのです。クリックするだけで、コントロール指標がリプレイ/シミュレーションサービスにアクションを要求します。しかし、リプレイ/シミュレーションの準備に追われている段階では、サービスは応答しません。

コードを見ると、システムが常にイベントに反応していることがわかります。そのため、イベントベースのシステムをサポートするためにEventCustom列挙体を作成しました。それを変えるつもりはありません。実際、そのような変更は検討すべきではありません。イベントを使うよりも、もっと複雑なアプローチをいくつも使わざるを得なくなるからです。しかし、単にイベントの存在を示す列挙を追加するだけでは解決にはなりません。何をすべきか見てみましょう。DispatchMessageプロシージャを変更し、サービスがビジー状態の場合、再生/一時停止ボタンを押してもイベントが発生しないようにしています。これは、以下のチェックを追加することで簡単に実装できます。

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
        {
                u_Interprocess Info;
                static int six = -1, sps;
                int x, y, px1, px2;
                                
                switch (id)
                {

// ... Internal code ...

                        case CHARTEVENT_OBJECT_CLICK:
                                if (m_bWait) break;
                                if (sparam == m_szBtnPlay)
                                {
                                        Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                        if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else
                                        {
                                                RemoveCtrlSlider();
                                                m_Slider.szBtnPin = NULL;
                                        }
                                        Info.s_Infos.iPosShift = (ushort) m_Slider.posPinSlider;
                                        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                        ChartRedraw();
                                }else   if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1);
                                else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1);
                                break;

// ... The rest of the code ....


このテスト行を追加することで、指標がビジー状態の時にサービスにリクエストを送信するのを防ぐことができます。というのも、再生/一時停止ボタンをクリックしても何も変化がないのは、ユーザーにとって好ましくないことだからです。他の行動を取らなければなりません。さらに、テスト対象の変数の値を正しく設定できていません。

この部分は少しわかりにくいかもしれませんが、実際にやるのは m_bWait変数の値を変更して確認するだけです。これにより、どの画像をプロットするかを決定することができます。目標は、再生/一時停止ボタンが、サービスがビジー状態の間は別の画像に変わり、サービスが無効になると従来の再生/一時停止ボタンに戻るようにすることです。シンプルなアプローチを使います。

void CreateBtnPlayPause(bool state)
{
        m_szBtnPlay = def_PrefixObjectName + "Play";
        CreateObjectBitMap(5, 25, m_szBtnPlay, (m_bWait ? def_ButtonWait : def_ButtonPause), (m_bWait ? def_ButtonWait : def_ButtonPlay));
        ObjectSetInteger(m_id, m_szBtnPlay, OBJPROP_STATE, state);
}



単に変数を確認しているだけであることにご注意ください。その値に応じて、再生/一時停止ボタンや待機信号を表すボタンを使用します。このボタンはどのように操作するのでしょうか。ターミナルからグローバル変数の値を常に読み込むのでしょうか。似たようなことをすると思います。これを忘れないでください。サービスが市場リプレイ資産に新しいレコードを追加するたびに、これは指標に反映されます。したがって、MetaTrader 5はOnCalculate関数を起動するイベントを生成します。そこで私たちの出番となるわけですが、常に指標を監視するわけではありません。もっとエレガントな方法でおこないます。この流れを理解するために、コード内の呼び出しの流れを示した下の画像をご覧ください。

これはまさに、コントロール指標のボタンを正しくコントロールするために実行される一連の動作です。CreateBtnPlayPauseプロシージャはすでに前に紹介したので、かなり自明だと思います。次に、この図の他の点を見てみましょう。OnCalculateプロシージャはより難しいロジックを含み、DispatchMessageでのステップを理解する必要があります。 

それでは、カスタムイベントを処理する基本的なコードに移りましょう。次のコードを見てみましょう。

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        u_Interprocess Info;
        static int six = -1, sps;
        int x, y, px1, px2;
                                
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_WAIT_ON):
                        m_bWait = true;
                        CreateBtnPlayPause(true);
                        break;
                case (CHARTEVENT_CUSTOM + Ev_WAIT_OFF):
                        m_bWait = false;
                        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                        CreateBtnPlayPause(Info.s_Infos.isPlay);
                        break;

// ... The rest of the code ...


DispatchMessageがコントロール指標のOnChartEventによって呼び出されると、MetaTrader 5プラットフォームによって提供されるイベントメッセージと、コードによって特定の時点でトリガーされるカスタムイベントの両方を処理できるようにデータが渡されます。カスタムイベントについては後述します。この関数はEv_WAIT_ONカスタムイベントが使用されている場合に一致するコードを探します。これでサービスがビジー状態であることがわかり、変数 m_bWaitがtrueになります。次に、ビジー状態を示す画像を実際にプロットする「再生/一時停止」ボタンの作成を呼び出します。Ev_WAIT_OFFカスタムイベントがトリガーされた場合、サービスの現在の状態、つまり再生モードか一時停止モードかを示したいのです。したがって m_bWait変数には、サービスがリクエストを受け付けることが可能であることを示す値が入ります。また、グローバルターミナル変数からデータを取得する必要があります。グローバルターミナル変数には、サービスの現在の状態が格納されます。次に、ユーザーがシステムと対話できるように、再生/一時停止ボタンを作成する関数を呼び出す。

この考え方は非常に直感的で、誰にでも理解できると思います。ただ、大きな懸念は、これらのイベントはどのようにトリガーされるのかということです。非常に複雑で理解しにくいコードになるのでしょうか。いいえ、MQL5でイベントをトリガーする方法は非常にシンプルで、前述のカスタムイベントを分析処理する方法も同様です。上記のコードでは、2つのカスタムイベントを処理する方法を見ることができます。では、これらのイベントをトリガーする方法を見てみましょう。カスタムイベントをトリガーするとき、実際にはOnChartEvent関数を呼び出しています。この関数は、カスタムイベントまたはMetaTrader 5から発生したイベントのいずれかが発生すると、常に呼び出されます。呼び出される関数は常に同じです。コマンドラインでこの関数のコードを見てみましょう。

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Control.DispatchMessage(id, lparam, dparam, sparam);
}



つまり、イベントが発生すると、その処理はC_Controlクラスに委譲され、DispatchMessage関数が実行されます。すべてがどのように機能しているか、お気づきでしょうか。DispatchMessage関数に含まれるコードがイベント処理関数内にあったとしても、結果は同じです。ただし、OnChartEvent関数は4つのパラメータを取りますが、カスタムイベントをトリガーする関数はより多くのパラメータを使うことにご注意ください。実際、カスタムイベントのトリガーに使われるパラメータは5つあります。こうすることで、カスタムイベントとMetaTrader 5からのイベントを区別することができるのです。注意すると、選択時に使用される値はEventCustom列挙で示される値と、その他のデータCHARTEVENT_CUSTOMとの合計であることがわかります。こうすれば正しい値が得られます。 

しかし、この価値はどのようにして生み出されるのでしょうか。MQL5を使用してカスタムイベントを生成するにはどうすればよいのかを理解するために、コントロール指標のメインコードであるOnCalcule関数をご覧ください。以下に示します。

int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        static bool bWait = false;
        u_Interprocess Info;
        
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (!bWait)
        {
                if (Info.s_Infos.isWait)
                {
                        EventChartCustom(m_id, Ev_WAIT_ON, 0, 0, "");
                        bWait = true;
                }
        }else if (!Info.s_Infos.isWait)
        {
                EventChartCustom(m_id, Ev_WAIT_OFF, 0, Info.u_Value.df_Value, "");
                bWait = false;
        }
        
        return rates_total;
}



上記のコードがどのように機能するかを理解しましょう。最初に注意すべきことは、このコードはMetaTrader 5から呼び出されるイベントハンドラであるということです。つまり、資産価格が変化したり、資産が新しい取引ティックを受信するたびに、OnCalcule関数はMetaTrader 5によって自動的に呼び出されます。したがって、指標内にタイマーを置く必要はありません。実際、指標でタイマーを使うのは(できるだけ)避けるべきです。なぜなら、タイマーは問題の指標だけでなく、他のすべての指標にも影響するからです。したがって、MetaTrader 5プラットフォームのこの呼び出しを使用して、サービスで何が起こっているかを確認します。このサービスは入力データをリプレイ/シミュレーションリソースに送信するため、OnCalcule関数を間接的に呼び出すことに注意してください。


結論

他のすべての基礎となるものなので、大まかな考え方は理解していただけたと願っています。OnCalculeを呼び出すたびに、末端のグローバル変数に値を書き込み、ローカルの静的変数がtrueかどうかを確認します。この値がtrueでない場合、サービスがビジー状態かどうかを確認します。この条件が満たされた場合、これを報告する特別なイベントを作成します。この直後、コントロール指標がリプレイ/シミュレーションサービスがビジー状態であることを知っていることを示すために、ローカルの静的変数の値を変更します。そこで、次にOnCalculeを呼び出すときに、リプレイ/シミュレーションサービスがそのアクティビティを実行するために空いているかどうかを確認します。これが起きたら、サービスがコントロール指標リクエストを受け取る準備ができたことを示す特別なイベントをトリガーします。そしてループは、ローカルの静的変数がtrueである限り繰り返されます。

ここで、カスタムイベントをトリガーするために一般的なもの、つまりEventChartCustom関数を使用していることにご注目ください。ここでは、現在のチャートと指標によってのみ制限されます。しかし、あらゆるチャート、指標、さらにはエキスパートアドバイザー(EA)に対してイベントをトリガーすることができます。そのためには、EventChartCustom関数のパラメータを正しく入力する必要があります。そうすれば、他のすべてはMetaTrader 5プラットフォームに委ねられ、指標またはEAでカスタムイベントを処理するだけでよくなります。これはあまり研究されていない側面であり、私が気づいたところでは、人々はMetaTrader 5プラットフォームが特定のアクションを実行できないと信じていることがあります。 

次のビデオでは、現在の開発段階にあるシステムのデモンストレーションをおこないます。MQL5言語が提供する機能だけでなく、MetaTrader 5プラットフォームについてよりよく学ぶのに役立つことを願っています。



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

添付されたファイル |
Market_Replay.zip (13060.83 KB)
リプレイシステムの開発 - 市場シミュレーション(第10回):リプレイで実データのみを使用する リプレイシステムの開発 - 市場シミュレーション(第10回):リプレイで実データのみを使用する
ここでは、リプレイシステムで、調整されているかどうかを気にすることなく、より信頼性の高いデータ(取引されたティック)を使用する方法を見ていきます。
リプレイシステムの開発 - 市場シミュレーション(第13回):シミュレーターの誕生(III) リプレイシステムの開発 - 市場シミュレーション(第13回):シミュレーターの誕生(III)
ここでは、次回以降の仕事に関連するいくつかの要素を簡略化します。シミュレーターが生成するランダム性を視覚化する方法も説明しましょう。
リプレイシステムの開発 - 市場シミュレーション(第11回):シミュレーターの誕生(I) リプレイシステムの開発 - 市場シミュレーション(第11回):シミュレーターの誕生(I)
バーを形成するデータを使うためには、リプレイをやめてシミュレーターの開発に着手しなければなりません。難易度が最も低い1分バーを使用します。
ニューラルネットワークが簡単に(第49回):Soft Actor-Critic ニューラルネットワークが簡単に(第49回):Soft Actor-Critic
連続行動空間の問題を解決するための強化学習アルゴリズムについての議論を続けます。この記事では、Soft Actor-Critic (SAC)アルゴリズムについて説明します。SACの主な利点は、期待される報酬を最大化するだけでなく、行動のエントロピー(多様性)を最大化する最適な方策を見つけられることです。