English Русский 中文 Español Deutsch Português
preview
リプレイシステムの開発 — 市場シミュレーション(第4回):設定の調整(II)

リプレイシステムの開発 — 市場シミュレーション(第4回):設定の調整(II)

MetaTrader 5 | 16 8月 2023, 10:37
490 0
Daniel Jose
Daniel Jose

はじめに

前回の記事「リプレイシステムの開発 - 市場シミュレーション(第3回):設定の調整(I)」では、市場再生サービスを簡単に管理できるEAを作成しました。今のところ、一時停止と再生という1つの重要ポイントを実装できています。希望する再生開始位置を選択できるようなコントロールは作成していません。つまり、途中から再生したり、特定の時点から再生したりすることはまだできません。常にデータの最初から始めなければなりません。これは、トレーニングをしたい人にとっては現実的ではありません。

今回は、可能な限りシンプルな方法で再生の開始点を選択する機能を実装します。また、このシステムを気に入り、自分のEAに使いたいという友人たちの要望により、戦略を少し変更する予定です。これを可能にするために、システムに適切な変更を加えます。

とりあえず、新しいアプリケーションが実際にどのように作成されるかを示すために、この方法を取ることにします。多くの人は、アイデアが生まれた瞬間から、システムやコードが完全に安定し、アプリケーションが期待通りの動作をするようになるまでの全過程を理解していません。


EAを指標に交換する

この変更はかなり簡単に実装できます。その後は、市場再生サービスを使ったリサーチやライブ市場での取引に独自のEAを使うことができるようになります。例えば、以前の記事で紹介したEAを使うことができるようになります。詳細は、連載「一からの取引エキスパートアドバイザーの開発」をご覧ください。100%自動化されるようには設計されていませんが、再生サービスで使用するために適合させることができます。これは将来のために残しておきましょう。さらに、連載「自動で動くEAを作る(第01回):概念と構造)」からいくつかのEAを使うこともできます。ここでは、完全に自動化されたモードで動作するEAを作成する方法を説明しました。

しかし、現在注目しているのは、EAではなく(将来的には検討する予定ですが)、別のことです。

完全な指標コードは以下で見ることができます。EAにすでに存在していた機能を、そのまま指標として実装しています。

#property copyright "Daniel Jose"
#property indicator_chart_window
#property indicator_plots 0
//+------------------------------------------------------------------+
#include <Market Replay\C_Controls.mqh>
//+------------------------------------------------------------------+
C_Controls      Control;
//+------------------------------------------------------------------+
int OnInit()
{
        IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay");
        Control.Init();
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Control.DispatchMessage(id, lparam, dparam, sparam);
}
//+------------------------------------------------------------------+


唯一の違いは、指標に含めることが望ましい短い名前を加えたことです。この部分は上のコードで強調表示されています。こうすることで、さらなるメリットが得られます。どんなEAでも、再生サービスで練習やトレーニングに使うことができるということです。市場の再生はストラテジーテスターではありません。市場を解読する練習をし、資産の動きに対する知覚を向上させることで安定を得たい人を対象としています。市場の再生は、素晴らしいMetaTrader 5ストラテジーテスターの代わりにはなりません。ただし、ストラテジーテスターは市場の再生の練習には適していません。

一見、副作用がなさそうに見えますが、そうとも言い切れません。エキスパートアドバイザー(EA)の代わりに指標で制御するようにリプレイシステムを実行すると、不具合が発生します。チャートの時間枠が変更されると、指標はチャートから削除され、再起動されます。この削除と再起動の操作によって、一時停止モードか再生モードかを示すボタンと、リプレイシステムの実際の状態が一致しなくなります。これを解決するには、ちょっとした調整が必要です。次のような指標の開始コードを用意します。

int OnInit()
{
        u_Interprocess Info;
        
        IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay");
        if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.Value = 0;
        Control.Init(Info.s_Infos.isPlay);
        
        return INIT_SUCCEEDED;
}


コード中で強調表示されている追加部分は、再生サービスの状態がチャート上に表示されるボタンと一致することを保証します。コントロールコードの変更は非常にシンプルで、特別な注意を払う必要はありません。

これで、これまでEAが使用していたテンプレートファイルが、指標の使用に切り替わります。このため、将来的に他の変更を加える自由は完全に残されています。


ポジションコントロールの実装

ここでは、再生ファイルのどこに移動して市場調査を開始するかを示すコントロールを実装しますが、位置は正確ではありません。開始位置はおおよその目安になります。しかし、できないからではやらないのではありません。逆に、正確な位置を示す方はるかに簡単でしょう。ただし、市場でより多くの経験を積んでいる人たちと話をし、経験を交換したとき、ひとつ同意したことがあったのです。理想的な選択肢は、すでに特定の動きが予想される正確な地点に行くのではなく、希望する動きに近い位置から再生を開始することです。つまり、行動を起こす前に何が起こっているのかを理解する必要があります。

このアイデアは私にとってとても良いものに思えたので、市場の再生は特定の位置にジャンプすべきではないと決めました。実装するのは簡単ですが、最も近い位置に行く必要があります。どの位置が最も近くなるかは、1日の取引回数によります。取引を実行すればするほど、的確に当てるのは難しくなります。

そこで、実際に取引シミュレーションを作成するために、近くの位置にアクセスして実際に何が起きているのかを理解します。繰り返しになりますが、私たちはストラテジーテスターを作っているのではありません。 こうすることで、時間をかけて、いつが安全な動きなのか、いつがリスクが高すぎて取引に参入すべきではないのかを判断できるようになります。

このステップのすべての作業は、C_Controlクラスの内部でおこないます。さあ、仕事に取り掛かりましょう。

まず最初に、いくつかのものを定義します。

#define def_ButtonLeft  "Images\\Market Replay\\Left.bmp"
#define def_ButtonRight "Images\\Market Replay\\Right.bmp"
#define def_ButtonPin   "Images\\Market Replay\\Pin.bmp"


次に、ポジションシステムのデータを格納するための変数セットを作成する必要があります。これらは以下のように実装されます。

struct st_00
{
        string  szBtnLeft,
                szBtnRight,
                szBtnPin,
                szBarSlider;
        int     posPinSlider,
                posY;
}m_Slider;


これはまさに、あなたが今気づいたことです。スライダーを使って、リプレイシステムを開始するおおよその位置を指定します。これで、再生/一時停止ボタンとスライダーボタンの作成に使用するジェネリック関数ができました。この関数を以下に示します。非常にシンプルなので、理解するのに苦労することはないと思います。

inline void CreateObjectBitMap(int x, int y, string szName, string Resource1, string Resource2 = NULL)
                        {
                                ObjectCreate(m_id, szName, OBJ_BITMAP_LABEL, 0, 0, 0);
                                ObjectSetInteger(m_id, szName, OBJPROP_XDISTANCE, x);
                                ObjectSetInteger(m_id, szName, OBJPROP_YDISTANCE, y);
                                ObjectSetString(m_id, szName, OBJPROP_BMPFILE, 0, "::" + Resource1);
                                ObjectSetString(m_id, szName, OBJPROP_BMPFILE, 1, "::" + (Resource2 == NULL ? Resource1 : Resource2));
                        }


これですべてのボタンがこの関数を使って作成されることになります。これにより、物事が非常に簡単になり、コードの再利用が増えます。次に作成するのは、スライダーで使用するチャンネルを表す関数です。以下の関数で作成されます。

inline void CreteBarSlider(int x, int size)
                        {
                                ObjectCreate(m_id, m_Slider.szBarSlider, OBJ_RECTANGLE_LABEL, 0, 0, 0);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XDISTANCE, x);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Slider.posY - 4);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XSIZE, size);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YSIZE, 9);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_WIDTH, 3);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT);
                        }


ここで最も興味深いのは、コントロールチャンネルの境界線の表現です。これは、OBJPROP_YSIZEプロパティで設定されるチャンネルの幅と同様に、必要に応じてカスタマイズできます。ただし、このプロパティの値を変更するときは、チャンネルがボタンの間に来るように、m_Slider.posYを引いた値を調整することを忘れないでください。

再生/一時停止ボタンを作成する関数は次のようになります。

void CreateBtnPlayPause(long id, bool state)
{
        m_szBtnPlay = def_PrefixObjectName + "Play";
        CreateObjectBitMap(5, 25, m_szBtnPlay, def_ButtonPause, def_ButtonPlay);
        ObjectSetInteger(id, m_szBtnPlay, OBJPROP_STATE, state);
}


はるかに簡単だと思います。次に、スライダーを作成する関数を以下に示します。

void CreteCtrlSlider(void)
{
        u_Interprocess Info;
                                
        m_Slider.szBarSlider = def_PrefixObjectName + "Slider Bar";
        m_Slider.szBtnLeft   = def_PrefixObjectName + "Slider BtnL";
        m_Slider.szBtnRight  = def_PrefixObjectName + "Slider BtnR";
        m_Slider.szBtnPin    = def_PrefixObjectName + "Slider BtnP";
        m_Slider.posY = 40;
        CreteBarSlider(82, 436);
        CreateObjectBitMap(52, 25, m_Slider.szBtnLeft, def_ButtonLeft);
        CreateObjectBitMap(516, 25, m_Slider.szBtnRight, def_ButtonRight);
        CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin);
        ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER);
        if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.Value = 0;
        PositionPinSlider(Info.s_Infos.iPosShift);
}


コントロールの名前をよくご覧ください。リプレイシステムが再生状態にある間は、これらのコントロールは使用できません。この関数は、一時停止状態になるたびに呼び出され、スライダーを作成します。このため、スライダーを正しく識別して配置するために、ターミナルのグローバル変数の値を取得することになります。

よって、手動でターミナルのグローバル変数に何もしないことをお勧めします。もうひとつの重要なディテール、ピンに注目してください。ボタンとは異なり、アンカーポイントがちょうど中央に来るように設計されているため、見つけやすくなっています。ここで、もうひとつの関数呼び出しがあります。

inline void PositionPinSlider(int p)
{
        m_Slider.posPinSlider = (p < 0 ? 0 : (p > def_MaxPosSlider ? def_MaxPosSlider : p));
        ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin);
        ChartRedraw();
}


スライダーを特定の領域に配置し、上記で設定した制限内に収まるようにします。

ご想像の通り、システムの微調整はまだ必要です。チャートの時間枠が変わるたびに、指標はリセットされ、リプレイシステム内で現在地を見失うことになります。これを避ける方法のひとつは、初期化関数にいくつかの追加をすることです。これらの変更点を生かし、さらにいくつかの追加もおこなう予定です。初期化関数を見てみましょう。

void Init(const bool state = false)
{
        if (m_szBtnPlay != NULL) return;
        m_id = ChartID();
        ChartSetInteger(m_id, CHART_EVENT_MOUSE_MOVE, true);
        CreateBtnPlayPause(m_id, state);
        GlobalVariableTemp(def_GlobalVariableReplay);
        if (!state) CreteCtrlSlider();
        ChartRedraw();
}


ここで、マウスの移動イベントを指標に転送するコードも追加します。この追加をおこなわないと、マウスイベントは失われ、MetaTrader 5から指標に渡されません。不要なときにスライダーを隠すために、小さなチェックを追加しました。このチェックでスライダーを表示することが確認されれば、画面に表示されます。

ここまで見てきて、現在イベント処理はどのようにおこなわれているのかを不思議に思われるかもしれません。超複雑な追加コードが必要になるのでしょうか。まあ、マウスイベントの処理方法はあまり変わりません。ドラッグイベントを追加するのは、それほど複雑なことではありません。本当にしなければならないのは、物事がコントロールできなくならないように、いくつかの制限を管理することです。実装自体はいたってシンプルです。

これらすべてのイベントを処理するDispatchMessage関数のコードを見てみましょう。説明を簡単にするために、コードを部分的に見てみましょう。最初の部分は、オブジェクトのクリックイベントを処理します。次のコードをご覧ください。

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        u_Interprocess Info;

//... other local variables ....
                                
        switch (id)
        {
                case CHARTEVENT_OBJECT_CLICK:
                        if (sparam == m_szBtnPlay)
                        {
                                Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else
                                {
                                        ObjectsDeleteAll(m_id, def_PrefixObjectName + "Slider");
                                        m_Slider.szBtnPin = NULL;
                                }
                                Info.s_Infos.iPosShift = m_Slider.posPinSlider;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.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...

再生/一時停止ボタンが押されたら、いくつかのアクションを実行する必要があります。そのひとつが、一時停止状態のスライダーを作ることです。一時停止状態を終了し、再生状態に入ったら、コントロールをチャートから隠し、アクセスできないようにしなければなりません。スライダーの現在値は、ターミナルのグローバル変数に送られなければなりません。このように、再生サービスは、リプレイシステムを配置する、あるいは配置したいポジションのパーセンテージを知ることができます。

再生/一時停止ボタンに関するこれらの問題に加えて、スクロールのポイント単位のシフトボタンをクリックしたときに起こるイベントにも対処する必要があります。スクロールの左ボタンをクリックすると、スライダーの現在値が1つ減るはずです。同様に、スクロールの右ボタンを押すと、設定された上限までコントロールに1つ追加されるはずです。

これはとてもシンプルなことです。少なくともこの部分では、オブジェクトのクリックメッセージを扱うのはそれほど難しくありません。しかし今、スライダーのドラッグには少し複雑な問題があります。これを理解するために、マウスの移動イベントを処理するコードを見てみましょう。以下に示します。

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)
        {

// ... Object click EVENT ...

                case CHARTEVENT_MOUSE_MOVE:
                        x = (int)lparam;
                        y = (int)dparam;
                        px1 = m_Slider.posPinSlider + def_MinPosXPin - 14;
                        px2 = m_Slider.posPinSlider + def_MinPosXPin + 14;
                        if ((((uint)sparam & 0x01) == 1) && (m_Slider.szBtnPin != NULL))
                        {
                                if ((y >= (m_Slider.posY - 14)) && (y <= (m_Slider.posY + 14)) && (x >= px1) && (x <= px2) && (six == -1))
                                {
                                        six = x;
                                        sps = m_Slider.posPinSlider;
                                        ChartSetInteger(m_id, CHART_MOUSE_SCROLL, false);
                                }
                                if (six > 0) PositionPinSlider(sps + x - six);
                        }else if (six > 0)
                        {
                                six = -1;
                                ChartSetInteger(m_id, CHART_MOUSE_SCROLL, true);
                        }
                        break;
        }
}


少し複雑に見えますが、実際にはオブジェクトのクリックを処理するのと同じくらい簡単です。唯一の違いは、より多くの変数を使わなければならなくなったことと、呼び出しの間に値が失われないように、変数のいくつかを静的にしなければならないことです。マウスが動かされると、MetaTrader 5は私たちのシステムにメッセージを送信します。このメッセージを使って、何が起こったのか、マウスカーソルがどこにあるのか、どのボタンが押されたのか、その他の情報を調べる必要があります。これらの情報はすべて、MetaTrader 5から私たちのアプリケーションに送信されるメッセージに含まれています。

左のボタンが押されると、何かがおこなわれます。しかし、スライダーがスクリーンに表示され、偽陽性が出ないことを確認するために、私たちは私たちが行っていることの完全性を保証するための追加テストを提供しています。

テストがイベントが有効であることを示した場合、別のテストを実行して、スライダー上、つまりスライダーに属する領域をクリックしているかどうかを確認します。同時に、この位置がまだ有効かどうかも確認します。クリックはすでにされているが、その位置が有効でないということが起こりうるからです。この場合は無視すべきです。 この確認が成功すれば、クリック位置とコントロール値の両方を保存します。チャートのドラッグもロックする必要があります。これは次のステップで、コントロールに存在する前の値に基づいてスライダーの位置を計算するために必要です。 計算の前にこのデータを保存しておくことは、セットアップを容易にし、この場合の進め方を理解する上で非常に重要です。しかし、ここでのやり方は、計算が実際に偏差の計算になるので、実装は非常に簡単です。 

左ボタンが離されたら、元のモードに戻ります。つまり、グラフは再びドラッグ可能になり、マウスの位置を格納するために使用される静的変数は、位置が分析されていないことを示す値を持つようになります。同じ方法でチャート上に何でもドラッグ&ドロップできます。すべてクリックとドラッグでおこないます。あとは、クリックを受けられる領域がどこかを分析するだけです。これを微調整すれば、あとはコードがやってくれます。上のコードのようになります。

こうすることで、コントロールに望ましい動作がすでに備わっています。だが、まだ終わってはいません。スライダーで指定した値を使うようにサービスを強制しなければなりません。次回のトピックでは、これを実装します。


C_Replayクラスの調整

物事は、ある人々が想像するようなまったく同じということはありません。スライダーを作成し、コントロールクラス(C_Control)で何かを設定したからといって、すべてが完璧に機能するわけではありません。実際に再生を構築するクラスには調整が必要です。

これらの調整はそれほど複雑なものではありません。実際、その数は非常に少ないし、非常に特定のポイントに限られています。しかし、一方のクラスに加えられた変更は、他方のクラスにも影響することに注意することが重要です。しかし、それ以外の点は必ずしも変更する必要はありません。私は、不必要な箇所には決して手を触れず、可能な限り常にカプセル化を最大レベルまで高め、システム全体の複雑さを隠すことを好みます。

本題に入りましょう。最初にすべきことは、Event_OnTime関数を設定することです。これはレプリケーション資産に取引されたティックを追加する役割を担っています。基本的には、この機能にちょっとしたことを追加するつもりです。下のコードをご覧ください。

#define macroGetMin(A)  (int)((A - (A - ((A % 3600) - (A % 60)))) / 60)
inline int Event_OnTime(void)
{
        bool isNew;
        int mili, test;
        static datetime _dt = 0;
        u_Interprocess Info;
                                
        if (m_ReplayCount >= m_ArrayCount) return -1;
        if (m_dt == 0)
        {
                m_Rate[0].close = m_Rate[0].open =  m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last;
                m_Rate[0].tick_volume = 0;
                m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60;
                CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                _dt = TimeLocal();
        }
        isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt;
        m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt);
        mili = m_ArrayInfoTicks[m_ReplayCount].milisec;
        do
        {
                while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec)
                {
                        m_Rate[0].close = m_ArrayInfoTicks[m_ReplayCount].Last;
                        m_Rate[0].open = (isNew ? m_Rate[0].close : m_Rate[0].open);
                        m_Rate[0].high = (isNew || (m_Rate[0].close > m_Rate[0].high) ? m_Rate[0].close : m_Rate[0].high);
                        m_Rate[0].low = (isNew || (m_Rate[0].close < m_Rate[0].low) ? m_Rate[0].close : m_Rate[0].low);
                        m_Rate[0].tick_volume = (isNew ? m_ArrayInfoTicks[m_ReplayCount].Vol : m_Rate[0].tick_volume + m_ArrayInfoTicks[m_ReplayCount].Vol);
                        isNew = false;
                        m_ReplayCount++;
                }
                mili++;
        }while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec);
        m_Rate[0].time = m_dt;
        CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
        mili = (m_ArrayInfoTicks[m_ReplayCount].milisec < mili ? m_ArrayInfoTicks[m_ReplayCount].milisec + (1000 - mili) : m_ArrayInfoTicks[m_ReplayCount].milisec - mili);
        test = (int)((m_ReplayCount * def_MaxPosSlider) / m_ArrayCount);
        GlobalVariableGet(def_GlobalVariableReplay, Info.Value);
        if (Info.s_Infos.iPosShift != test)
        {
                Info.s_Infos.iPosShift = test;
                GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
        }
                        
        return (mili < 0 ? 0 : mili);
};
#undef macroGetMin


この関数では、1分足バーを構築します。この部分で変数を追加していることにご注意ください。この変数は上のコードには存在しませんでした。これで、ターミナルのグローバル変数に相対的なパーセンテージの位置が格納されたことになります。従って、ターミナル変数に格納されている内部コンテンツをデコードするために、この変数が必要です。取引されたティックが1分足に追加されたら、現在の再生ポジションが何パーセントであるかを知る必要があります。これはこの計算でおこなわれ、保存されたティックの総数に対する相対的な位置を求めます。

そしてこの値は、ターミナルのグローバル変数に格納されている値と比較されます。異なる場合は、システムが停止したときに正しい相対位置を示すように値を更新します。そうすれば、余計な計算をしたり、不必要な問題に遭遇したりすることはありません。

これで第1ステージは終了です。しかし、もうひとつ解決しなければならない問題があります。一時停止中に値を調整した後、リプレイシステムを希望の相対位置に配置するにはどうすればいいでしょうか。

この問題はもう少し複雑です。解くのが簡単な足し算と、少し複雑な引き算の両方があるからです。この引き算は、少なくとも現段階では大きな問題ではありません。しかし、これは次回の記事でわかることですが、次の段階で問題になります。しかし、最初にすべきことは、C_Replayクラスに、リプレイシステムから小節を追加または減算するための追加関数を追加することです。この関数の準備を見てみましょう。

int AdjustPositionReplay()
{
        u_Interprocess Info;
        int test = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_ArrayCount);
                                
        Info.Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == test) return 0;
        test = (int)(m_ArrayCount * ((Info.s_Infos.iPosShift * 1.0) / def_MaxPosSlider));
        if (test < m_ReplayCount)
        {
                CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
                CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
                m_ReplayCount = 0;
                m_Rate[0].close = m_Rate[0].open =  m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last;
                m_Rate[0].tick_volume = 0;
                m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60;
                CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
        };
        for (test = (test > 0 ? test - 1 : 0); m_ReplayCount < test; m_ReplayCount++)
                Event_OnTime();

        return Event_OnTime();
}


上のコードに、このカスタマイズシステムの基礎となるコードがあります。この基本システムで何が起きているのかを理解しましょう。まず、現在位置のパーセント値を生成します。そして、この値をターミナルのグローバル変数にある値と比較します。制御システムは、グローバル変数に格納されたこの値を記録します。値が等しい場合(絶対値ではなくパーセンテージ値)、正しいパーセンテージポイントにいるか、システム休止中にユーザーが位置を変えなかったため、関数は終了します。

しかし、値が異なる場合は、ターミナルのグローバル変数で指定されたパーセント値に基づいて絶対値が生成されます。つまり、リプレイシステムが開始すべき絶対的なポイントを持つことになります。この値は、いくつかの理由から、取引刻みのカウンタと等しくなる可能性は低くなります。この値が現在の再生カウンタの値より小さい場合、現在のリソースに存在するすべてのデータが削除されます。

これは厄介なことですが、今の段階ではそうではありません。それは次のステップでおこなわれます。今のところ、過度に心配する必要はありません。つまり、再生カウンタの位置が絶対位置から1を引いた値に等しくなるまで、新しい値を追加するのです。このマイナス1は、この関数が後で遅延として使われる値を返すようにするためです。これはEvent_OnTime関数によって実現されます。 

このような変化には痛みが伴います。システムのどこを修正する必要があるのか見てみましょう。これは以下のコードに示されています。変わったのはここだけです。

#property service
#property copyright "Daniel Jose"
#property version   "1.00"
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string    user01 = "WINZ21_202110220900_202110221759"; //File with ticks
//+------------------------------------------------------------------+
C_Replay        Replay;
//+------------------------------------------------------------------+
void OnStart()
{
        ulong t1;
        int delay = 3;
        long id;
        u_Interprocess Info;
        bool bTest = false;
        
        if (!Replay.CreateSymbolReplay(user01)) return;
        id = Replay.ViewReplay();
        Print("Wait for permission to start replay ...");
        while (!GlobalVariableCheck(def_GlobalVariableReplay)) Sleep(750);
        Print("Replay system started ...");
        t1 = GetTickCount64();
        while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.Value)))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest) bTest = (Replay.Event_OnTime() > 0);
                }else
                {
                        if (bTest)
                        {
                                delay = ((delay = Replay.AdjustPositionReplay()) == 0 ? 3 : delay);
                                bTest = false;
                                t1 = GetTickCount64();
                        }else if ((GetTickCount64() - t1) >= (uint)(delay))
                        {
                                if ((delay = Replay.Event_OnTime()) < 0) break;
                                t1 = GetTickCount64();
                        }
                }
        }
        Replay.CloseReplay();
        Print("Replay system stopped ...");
}
//+------------------------------------------------------------------+


一時停止モードに入っている間に、サービスの状態を変更しているかどうかを確認するために、このテストを実行します。この場合、C_Replayクラスに新しいポジショニングの実行を依頼するが、実行されるかどうかはわかりません。

実行された場合、この調整がおこなわれ、システムが配置された後に使用される次のディレイの値が得られます。必要であれば、再生状態を終了してポーズ状態に入るまで、残りの時間を自然に継続します。その後、すべての手順をもう一度繰り返します。


結論

ビデオでは、システム全体が作動している様子を見ることができます。ただし、リプレイシステムを使用する前に、状況が安定するまで待つ必要があることには注意が必要です。ポジションを目的のポイントに移動させるとき、その動きは難しく感じるかもしれません。

この状況は将来的に改善されるでしょう。でも、まだ解明しなければならないことがたくさんあるので、今はこれでいいでしょう。



添付ファイルには、取引ティック数が異なる日の動きとポジショニングシステムを実験できるように、2つの実際の市場ティックファイルが含まれています。パーセンテージシステムがどのように機能するかはご覧になれます。市場の特定の瞬間を研究したい人にとっては複雑なことですが、冒頭で説明したように、これこそが私たちの意図です。

私たちがここで構築しているこのリプレイシステムを使えば、市場を分析する方法を本当に学ぶことができます。「ここだ...ここでエントリするべきだ」という正確な場所はないでしょう。 というのも、観察した動きが、実際には数バー先で起こるかもしれないからです。したがって、市場の分析方法を学ばなければなりません。そうでなければ、この連載で紹介するリプレイシステムはお気に召さないかもしれません。


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

添付されたファイル |
Market_Replay.zip (10795.89 KB)
MQL5の圏論(第14回):線形順序を持つ関手 MQL5の圏論(第14回):線形順序を持つ関手
この記事は、MQL5における圏論の実装に関する広範な連載の一部であり、関手について掘り下げます。関手のおかげで線形順序が集合にどのように写像できるかを検証します。一般的には何のつながりもないと見なされてしまうような2つのデータ集合について考えます。
MQL5の圏論(第13回):データベーススキーマを使用したカレンダーイベント MQL5の圏論(第13回):データベーススキーマを使用したカレンダーイベント
この記事は、MQL5での順序の圏論実装に従うもので、MQL5での分類のためにデータベーススキーマをどのように組み込むことができるかを検討します。取引関連のテキスト(文字列)情報を特定する際に、データベーススキーマの概念を圏論とどのように組み合わせることができるかの基礎を見ていきます。カレンダーイベントが中心です。
改善された同事ローソク足パターン認識指標に基づく取引戦略 改善された同事ローソク足パターン認識指標に基づく取引戦略
メタバーベースの指標は、従来のものよりも多くのローソク足を検出しました。これが自動売買に本当に役立つのか、検証してみましょう。
MQL5における圏論(第12回):順序 MQL5における圏論(第12回):順序
この記事は、MQL5でのグラフの圏論実装に従う連載の一部であり、順序について詳しく説明します。2つの主要な順序タイプを検討することで、順序理論の概念が取引の意思決定に情報を提供する上で、モノイド集合をどのようにサポートできるかを検証します。