English Русский 中文 Español Deutsch Português
preview
ティッカーテープパネルの作成:基本バージョン

ティッカーテープパネルの作成:基本バージョン

MetaTrader 5トレーディング | 20 2月 2023, 16:01
370 0
Daniel Jose
Daniel Jose

はじめに

いくつかのプラットフォームに組み込まれている、個々の資産の相場を表示するプライステープパネルがとてもクールだと感じる方もいらっしゃるかもしれませんが、何のことかわからない方は、下のGIFをご覧ください。

場合によっては、そうしたものが非常に有効なこともあります。ここでは、100%MQL5プログラミングで、MetaTrader 5プラットフォーム内にそのような要素を実装する方法を紹介します。多くの方は、この記事の内容がかなり簡単だと思われるかもしれませんが、ここで紹介する概念を理解すれば、もっと複雑なものを作れるようになることは保証します。

さらに、このパネルをさらに発展させ、リアルタイムで取引や情報をフォローしたい方にとって非常に便利なツールになるよう、別の記事も書いていくつもりです。

この記事のアイデアがこのコミュニティのメンバーの1人から提案されたものであることを認めないといけません。このアイデアは、実装や開発が非常に面白く、また多くの人にとって非常に有用なリソースとなる可能性があります。これが、私がこのようなパネルのコードを作成する方法を紹介することにした理由です。


計画

このようなパネルを作るのは、それほど難しいことではありません。実際、他のコードタイプに比べ、実装は非常に簡単です。ただし、実装に進む前に、パネル制作の方向性を大きく左右するいくつかの事柄を計画しておきましょう。最初から大きな苦労をせずに、資産とその価格を表示するパネルを持つことが目的ですので、以下のことはご勘弁ください。ここでは、より複雑で洗練された精巧なものを作るための出発点となる、非常に基本的なシステムの作り方を紹介します。

まず考えるべきなのは、パネルに表示される資産リストをどう扱うかです。これは、あらかじめ資産が選択された固定リストになるのでしょうか。それとも、システムを導入しながら銘柄を1つずつ挿入していくのでしょうか。

興味のある資産を持ちたい場合もあれば、ポートフォリオに入っている資産を観察したい場合もあるので、ここが一番難しいところでしょう。したがって、クウォートパネルに表示させたいすべての資産を含む1つのファイルを使用する方がよいかもしれないので、表示する資産を格納したファイルを使用することにします。

ここで、リソースをどのように表現するかという、もう1つの問題が発生します。小さなことのように思えますが、これを考えるのは実はとても大切です。エキスパートアドバイザー(EA)、スクリプト、インジケータ、サービスを利用することができますが、後者は明白な解決策とは思えません。私としては、個人的にはサービスを利用するのが好きですが、それをパネルの実装に選ぶと、複雑な細部や困難が多すぎて、パネルの開発が複雑で時間のかかる作業になってしまいます。そのため、パネルを実装するには、EAに入れるか、インジケータに入れるかの2つの実用的な選択肢に絞られます。しかし、なぜスクリプトではダメなのでしょうか。

理由は簡単で、ユーザーが時間枠を変更しようと思ったら、最終的にはスクリプトを閉じなければならないからです。そのため、チャートが変わるたびに、トレーダーはスクリプトを再度実行する必要があります。これまで述べてきたように、これは100%MQL5によるソリューションになる予定です。外部のプログラミングソリューションを利用する方法も考えられますが、ここでの目的は異なります。

つまり、EAとインジケータの2つの選択肢が残されているわけです。私はEAを本来の目的である注文の送信と制御のために使用するのが好きなので、EAをここで使うアイディアは好きではありません。したがって、残された解決策はただ1つ、インジケータを使うことです。

まだ他にも検討計画しなければならない問題がありますが、すでにこの予備計画で始めることができます。始めましょう。


基本原則

まずは、インジケータファイルの作成から始めましょう。

#property copyright "Daniel Jose"
#property description "Program for a panel of quotes."
#property description "It creates a band that displays asset prices."
#property description "For details on how to use it visit:\n"
#property description "https://www.mql5.com/ru/articles/10941"
#property link "https://www.mql5.com/ru/articles/10941"
#property indicator_separate_window
#property indicator_plots 0
//+------------------------------------------------------------------+
int OnInit()
{
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+
void OnTimer()
{
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
}
//+------------------------------------------------------------------+

このインジケータのコードは完全にクリーンで、特別なことをするわけではありませんが、この先どうなるかは、すでにある程度予想がついています。例えば、別のインジケータウィンドウを使用し、通常はインジケータに表示されないOnTimeなど、古典的なインジケータよりも多くのイベントを処理する必要があります。次のことを忘れないでください。インジケータが作成するものは全てインジケータがおこなうので、まったく何もプロットしません

通常は、すぐに使えるコードから始めるのですが、今回は、読者が調査や勉強のための資料として使えるように、少しレベルを変えてすべてを紹介したいと思います。

すべてをうまく機能させるためには、さまざまなことを実装する必要があると、すでにお考えでしょう。ある意味そうなのですが、そんなにたくさんはありません。まず考えるべきなのは、チャートの管理方法です。そのためにはクラスがあります。このクラスは以前の記事でも紹介しましたが、今回は使用するものが少ないので、少し違った印象になります。C_Terminalクラスを知らないすべての方に提示します。このクラスは、C_Terminal.mqhヘッダーファイルにあります。そのコードは非常にシンプルです。以下でご覧ください。

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
class C_Terminal
{
//+------------------------------------------------------------------+
        private :
                struct st00
                {
                        long    ID;
                        int     Width,
                                Height,
                                SubWin;
                }m_Infos;
//+------------------------------------------------------------------+
        public  :
//+------------------------------------------------------------------+          
                void Init(const int WhatSub)
                        {
                                ChartSetInteger(m_Infos.ID = ChartID(), CHART_EVENT_OBJECT_DELETE, m_Infos.SubWin = WhatSub, true);
                                Resize();
                        }
//+------------------------------------------------------------------+
inline long Get_ID(void)   const { return m_Infos.ID; }
inline int GetSubWin(void) const { return m_Infos.SubWin; }
inline int GetWidth(void)  const { return m_Infos.Width; }
inline int GetHeight(void) const { return m_Infos.Height; }
//+------------------------------------------------------------------+
                void Resize(void)
                        {
                                m_Infos.Width = (int) ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
                                m_Infos.Height = (int) ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
                        }
//+------------------------------------------------------------------+
inline string ViewDouble(double Value)
                        {
                                Value = NormalizeDouble(Value, 8);
                                return DoubleToString(Value, ((Value - MathFloor(Value)) * 100) > 0 ? 2 : 0);
                        }
//+------------------------------------------------------------------+
                void Close(void)
                        {
                                ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, m_Infos.SubWin, false);
                        }
//+------------------------------------------------------------------+          
};
//+------------------------------------------------------------------+

そうです。必要なコードはこれで全部です。実際にはクラスはもっと大きいのですが、ここでは余分な資料で記事を埋め尽くしたくないので、必要な部分のみを紹介しています。

では、このクラスが何をするのかがわからない方のために、簡単に紹介します。オブジェクトを削除する試みについてMetaTrader 5に通知させたいので、ここでオブジェクトを宣言し、使用するウィンドウのサイズをキャプチャする必要があります。ここでは、プログラミングに役立つような抽象的なレベルを実際に作っています。

これは必須事項ではないため、別の方法で実装することができます。しかし、この抽象化レベルのおかげで、実際に組み立てられていないものを隠すことができるので、クラスデータにアクセスするための呼び出しがいくつかあります。クラスの最後には、インジケータがオブジェクトの削除を開始する間、イベントの発生を回避する必要がありますコードの中でフォーマットを作成する必要がある箇所があります。ここでそれをおこなうのは、ターミナルに関連するすべてを1つのクラスにまとめるためです。

ここまでは至ってシンプルです。ここからは複雑になるので、注意が必要です。


主要なオブジェクトの実装

奇妙に思えるかもしれませんが、パネルの基本モデルでは、たった2つのオブジェクトしか使用しないつもりです。以前、MQL5.communityで公開した連載「一からの取引エキスパートアドバイザーの開発」にあるモデルを使用しているので、そのモデルを取り上げることにします。この連載では、すべての仕組みについて詳しく説明していますが、ここでは、システムの動作について簡単に説明します。MetaTrader 5のオブジェクトの扱い方を知らなくても、迷うことはないでしょう。

では、まずベースとなるオブジェクトクラスから説明すると、そのコードは次のようになります。

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "..\Auxiliar\C_Terminal.mqh"
//+------------------------------------------------------------------+
class C_Object_Base
{
        public  :
//+------------------------------------------------------------------+
virtual void Create(string szObjectName, ENUM_OBJECT typeObj)
                        {
                                ObjectCreate(Terminal.Get_ID(), szObjectName, typeObj, Terminal.GetSubWin(), 0, 0);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTABLE, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTED, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, true);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
                        };
//+------------------------------------------------------------------+
                void PositionAxleX(string szObjectName, int X)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XDISTANCE, X);
                        };
//+------------------------------------------------------------------+
                void PositionAxleY(string szObjectName, int Y, int iArrow = 0)
                        {
                                int desl = (int)ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, (iArrow == 0 ? Y - (int)(desl / 2) : (iArrow == 1 ? Y : Y - desl)));
                        };
//+------------------------------------------------------------------+
virtual void SetColor(string szObjectName, color cor)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor);
                        }
//+------------------------------------------------------------------+
                void Size(string szObjectName, int Width, int Height)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XSIZE, Width);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE, Height);
                        };
//+------------------------------------------------------------------+
};

コードはシンプルでコンパクトです。このように抽象化することで、後で使用するコードを大幅に減らすことができます。ここでは、仮想関数があり、これは非常に汎用的な方法で任意のオブジェクトを作成する役割を担っています。ただし、この基本モデルでは1つのオブジェクトしか使わないので、この関数は少し無駄だと思われるかもしれません。これは真実ではなく、EAの注文システムのコードを見ていただければ、私が何を言っているのかご理解いただけると思います。

このほか、オブジェクトをチャート上に配置するための関数を2つ用意しました。また、オブジェクトの色を変更するための関数がありますが、これもオブジェクト作成関数と同様、仮想です。これが必要なのは、オブジェクトの中には複雑な色彩パターンを持つものがあるためです。そして最後に、オブジェクトの寸法を調整する関数を用意しました。

一見バカバカしく思えますが、この抽象化レベルを作ることで、どんなオブジェクトでも一意に扱えるようになり、将来的に役立ちます。これにはメリットがありますが、それはまた別の機会に紹介したいと思います。では、どのオブジェクトを選択してパネルを作成するかを見てみましょう。OBJ_EDITを選択することにします。その全コードを以下に示します。

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Base.mqh"
//+------------------------------------------------------------------+
#define def_ColorNegative       clrCoral
#define def_ColoPositive        clrPaleGreen
//+------------------------------------------------------------------+
class C_Object_Edit : public C_Object_Base
{
        public  :
//+------------------------------------------------------------------+
                template < typename T >
                void Create(string szObjectName, color corTxt, color corBack, T InfoValue)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_EDIT);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_FONT, "Lucida Console");
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_FONTSIZE, 10);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_ALIGN, ALIGN_LEFT);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, corTxt);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, corBack);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_COLOR, corBack);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_READONLY, true);
                                if (typename(T) == "string") ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, (string)InfoValue); else SetTextValue(szObjectName, (double)InfoValue);
                        };
//+------------------------------------------------------------------+
                void SetTextValue(string szObjectName, double InfoValue, color cor = clrNONE)
                        {
                                color clr;
                                clr = (cor != clrNONE ? cor : (InfoValue < 0.0 ? def_ColorNegative : def_ColoPositive));                                
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, Terminal.ViewDouble(InfoValue < 0.0 ? -(InfoValue) : InfoValue));
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, clr);
                        };
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+
#undef def_ColoPositive
#undef def_ColorNegative
//+------------------------------------------------------------------+

これだけですか。はい、EAの注文システムで使用されているコードとは若干異なりますが、これだけです。MQL5でよく使用するdouble型の値を格納する関数があり、編集型オブジェクトObj_Editも作成します。そしてもう1つ、初心者のプログラマーが混乱するようなことがあります。以下のコードで、オブジェクト作成関数を詳しく見てみましょう。

template < typename T >
void Create(string szObjectName, color corTxt, color corBack, T InfoValue)

実は、コンパイラはこの2行を1行として扱っています。何が起こっているのかがお分かりになるでしょうか。私がややこしくしているとお考えでしょうか。

template <typenameT > を使用します。ここでTは、現在の命名規則に従っている限り、他のものに置き換えることができます。これは、オーバーロードの一種を定義しています。同じような関数を作って、異なる引数やデータ型を受け取らなければならないことはよくあることです。これは極めて一般的なことです。そこで、そんな時に便利なように、こんな構文を使っています。不思議に思われるかもしれませんが、関数の内部は同じなのに、データの一部だけが違っていて、関数全体を書き直したくない場合によく使われる方法です。

注意深く見ると、プロシージャの最後に1行だけ、興味深いコードが書かれていることがわかります。

if (typename(T) == "string") ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, (string)InfoValue); else SetTextValue(szObjectName, (double)InfoValue);

このコードでは、 InfoValue変数に通知されたデータの型を確認します。ここで話しているのは値ではなく型についてです。この2つの の概念を混同しないようにご注意ください。

型が文字列の場合は、あるコードが実行され、型が異なる場合は、別のコードが実行されますが、これはコンパイラやリンカーがおこなうことではありません。この解析は通常実行時でおこなわれるので、リンカーが正しく処理をセットアップできるように、どのデータを処理するかを明示的に指示する必要があります。これはコード内で強調表示されている行を使用しておこなわれます

そこで、たった1つの違いでほとんど同じ関数を2つ作るのではなく、オーバーロードして必要なところを微調整し、最終的にはかなり手間を省くことができます。

この方法は、関数がdoubleである基本型でのみ機能するEAコードでは必要ありませんでしたが、今回はdoubleに加えてstringも扱うので、2つの型を実装するためだけにコードを重複させたくはなかったのです。

もっと詳しく知りたい方は、テンプレート関数をご確認ください。この情報は、なぜ関数のオーバーロードが頻繁に使われるのか、また、異なる型を使うからといって、すべてのコードを書き直す必要がないようにする方法を理解するのに役立ちます。

ただし、このオブジェクトに関連するセクションを終える前に、パネルの一番下にあるオブジェクトに注目してください。背景を作成する必要があります。背景がなければ、すべてがうまく機能しないと思っていらっしゃると思いますが、心配なさらないでください。このためのコードはとても簡単です。以下でご確認ください。

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Base.mqh"
//+------------------------------------------------------------------+
class C_Object_BackGround : public C_Object_Base
{
        public:
//+------------------------------------------------------------------+
                void Create(string szObjectName, color cor)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_RECTANGLE_LABEL);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
                                this.SetColor(szObjectName, cor);
                        }
//+------------------------------------------------------------------+
virtual void SetColor(string szObjectName, color cor)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, cor);
                        }
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+

このコードはシンプルでわかりやすいので、説明は不要かと思います。使用されるのはパネル背景の作成でのみです。とにかく、背景を作成するコードがどのようなものか疑問に思われる方がいらっしゃるかもしれないので、ここで紹介します。

これで、このセクションを終えることができます。すでにオブジェクトは実装されており、端末のサポート体制も整っているので、次のステップに進むことができます。


メインクラスの実装

ここまでの準備で、実際にシステムを動かすための最もエキサイティングなステップに入りました。関連するコードは、ヘッダーファイルC_Widget.mqhにあります。まずは、以下のような初期宣言から始めましょう。

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "Elements\C_Object_Edit.mqh"
#include "Elements\C_Object_BackGround.mqh"
//+------------------------------------------------------------------+
C_Terminal Terminal;
//+------------------------------------------------------------------+
#define def_PrefixName          "WidgetPrice"
#define def_NameObjBackGround	def_PrefixName + "BackGround"
#define def_MaxWidth            80
//+------------------------------------------------------------------+
#define def_CharSymbol          "S"
#define def_CharPrice           "P"
//+------------------------------------------------------------------+
#define macro_MaxPosition (Terminal.GetWidth() >= (m_Infos.nSymbols * def_MaxWidth) ? Terminal.GetWidth() : m_Infos.nSymbols * def_MaxWidth)
#define macro_ObjectName(A, B) (def_PrefixName + (string)Terminal.GetSubWin() + A + "#" + B)
//+------------------------------------------------------------------+

ここでは実際に必要なヘッダーファイルを宣言していますが、他のものもあります。そのすべてが必要なわけではありません。これらのヘッダーファイルが残りのすべてをカバーしているので十分です。

また、ターミナルクラスを宣言して、パネルを作成するのに使用します。また、このC_Widget.mqhヘッダーファイルには、使用する宣言マクロがいくつか用意されています。しかし、マクロは正しい方法で使用しなければならないので、十分注意してください。使い方を間違えなければ、大きな問題はなく、大いに助けになるでしょう。

これが完了したら、初期変数を含むクラスを宣言します。

class C_Widget
{
        protected:
                enum EventCustom {Ev_RollingTo};
        private :
                struct st00
                {
                        color   CorBackGround,
                                CorSymbol,
                                CorPrice;
                        int     nSymbols,
                                MaxPositionX;
                        struct st01
                        {
                                string szCode;
                        }Symbols[];
                }m_Infos;

この列挙は、必須ではありませんが、後で非常に役に立ちます。あると、物事の抽象度が上がるので、とても便利です。コードはより読みやすく、より理解しやすくなります。後で、いくつかのものを制御するのに役立つ構造体を宣言しますが、今はそれを心配する必要はありません。ただ、それがここにあり、クラスの完全にprivateな部分であること、つまり、外部のコードがそれにアクセスすることができないことを知っていれば大丈夫です。

さて、実際の動作に移りますが、その第一弾は以下の通りです。

void CreateBackGround(void)
{
        C_Object_BackGround backGround;
                        
        backGround.Create(def_NameObjBackGround, m_Infos.CorBackGround);
        backGround.Size(def_NameObjBackGround, Terminal.GetWidth(), Terminal.GetHeight());
}

ここでは、実際にパネルの背景を作成しますサブウィンドウの全領域を使用することに注意してください。ここにオブジェクトを配置し、すべてを1色で塗りつぶします。こうすれば、均一な背景を得ることができます。前の章で述べたように、ある種の抽象化をおこなうことで、より少ないプログラミングで、より速く結果を得ることができるようになるのです。さて、次はもっと複雑な話に移ります。

void AddSymbolInfo(const string szArg, const bool bRestore = false)
        {
#define macro_Create(A, B, C)   {                                               \
                edit.Create(A, m_Infos.CorSymbol, m_Infos.CorBackGround, B);    \
                edit.PositionAxleX(A, def_MaxWidth * m_Infos.nSymbols);         \
                edit.PositionAxleY(A, C);                                       \
                edit.Size(A, def_MaxWidth - 1, 22);                             \
                                }
                        
                C_Object_Edit edit;

                macro_Create(macro_ObjectName(def_CharSymbol, szArg), szArg, 10);
                macro_Create(macro_ObjectName(def_CharPrice, szArg), 0.0, 32);
                if (!bRestore)
                {
                        ArrayResize(m_Infos.Symbols, m_Infos.nSymbols + 1, 10);
                        m_Infos.Symbols[m_Infos.nSymbols].szCode = szArg;
                        m_Infos.nSymbols++;
                }
#undef macro_Create
        }

この関数では、ここでしか使わないマクロを宣言していますマクロは関数の外では使われないので、終了する前に削除していることに注意してください。

ここでは、 C_Object_Edit型のオブジェクトを作成し、一時的に配置し、その大きさを知らせています。これらはすべて、マクロの中でおこなわれます。これらの箇所では、実質的に全体の処理が同じであるため、コードを読みやすくするためにマクロを使用しています。もちろん、値の問題はありますが、機能は同じなので、マクロを使うわけです。ここでも、タイピングを減らし、生産性を向上させました。

さて、重要な内容に移りましょう。削除してはいけないオブジェクトをユーザが削除したときにも、同じ関数が呼び出されます。この場合、それ以降の行は実行されません。ただし、これらは通常の作成時に実行されるものです。まずメモリを確保し、確保した位置に銘柄の名前を入れ次の呼び出しのためにインクリメントします。そうすれば、次の呼び出しに移ることができます。

次にコードにあるのは、次のような面白い関数です。

inline void UpdateSymbolInfo(const int x, const string szArg)
{
        C_Object_Edit edit;
        string sz0 = macro_ObjectName(def_CharPrice, szArg);
        MqlRates Rate[1];
                                
        CopyRates(szArg, PERIOD_M1, 0, 1, Rate);                                
        edit.PositionAxleX(macro_ObjectName(def_CharSymbol, szArg), x);
        edit.SetTextValue(sz0, Rate[0].close, m_Infos.CorPrice);
        edit.PositionAxleX(sz0, x);
}

多くの人は、グローバルレベルでオブジェクトが必要だと考えていますが、実はMetaTrader 5内やMQL5を使用する際に必要なのです。作成されたオブジェクトはすべて必要に応じて操作できるため、そうとは言い切れません。オブジェクトの名前を調べるには、銘柄チャート上に存在するすべてのオブジェクトのリストが表示されたウィンドウを確認します。このように、名前を知っていれば、ローカルアクセスを使って、チャート上に存在するオブジェクトを操作することができるのです

そして、そのオブジェクトを操作できるようにするために、オブジェクトの名前を作成します。簡単にするために、マクロを使うことにします。.そのあと、もうひとつ面白いことがあります。通常、気配値表示ウィンドウには、情報を取得したい資産が表示されている必要がありますが、ここでの場合、パネルを作るときに、気配値表示で何百もの資産を開いて保持しなければいけないとすれば、ユーザーのモチベーションを下げてしまいます。これを避けるために、別の方法を使いますが、これには代償があります。タダというものはないのです。代償は、何が起こったかを知るためにこの関数を呼び出すたびに、最後のバーをコピーしなければいけないということです。

その後、必要なオブジェクトを適切な位置に配置し、オブジェクトをプロットする値を通知します。しかし、呼び出すたびに、実行にわずかな遅れが生じることを忘れないでください。これについては、この記事の後半で少し改善します。

次の関数は以下の通りです。

bool LoadConfig(const string szFileConfig)
{
        int file;
        string sz0;
        bool ret;
                                
        if ((file = FileOpen("Widget\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
                PrintFormat("Configuration file %s not found.", szFileConfig);
                return false;
        }
        m_Infos.nSymbols = 0;
        ArrayResize(m_Infos.Symbols, 30, 30);
        for (int c0 = 1; (!FileIsEnding(file)) && (!_StopFlag); c0++)
        {
                if ((sz0 = FileReadString(file)) == "") continue;
                if (SymbolExist(sz0, ret)) AddSymbolInfo(sz0); else
                {
                        FileClose(file);
                        PrintFormat("Ativo na linha %d não foi reconhecido.", c0);
                        return false;
                }
        }
        FileClose(file);
        m_Infos.MaxPositionX = macro_MaxPosition;
                
        return !_StopFlag;
}

ここでは、パネルで使用されるすべての資産を含むファイルを読み込みます。なお、拡張子は必要ありません。ファイルが置かれる場所を指定するだけです。このように、ファイルには任意の名前をつけることができるので、いろいろなものに対応したファイルを用意することができます。

ただし、正しいデータを持つファイルを使用しないと、何らかの問題が発生する可能性があるので注意が必要です。添付ファイルでは、システムのフルコードに加え、内部フォーマットを示すファイルを追加しています。このファイルには、現在Ibovespa指数(IBOV)に含まれているすべての資産が含まれています。このファイルを基に、他のすべてのファイルを作成します。このシステムには同じフォーマットが使用されており、他のすべてのアップデートや改良にも使用される予定です。

ファイルが見つかり、それを開くことができれば、到着したデータを格納するためのメモリを確保するために呼び出しを実行します。そして、1行ずつ読み始め、ファイルの終わりまで、あるいはユーザーが操作を中断するまで読み続けます行が空であったり、情報がない場合は、新たに読む呼び出しをおこないますもう 1 つ重要な点があります。資産は存在する場合にのみ追加されます。 存在しない場合は、エラーが発生した行を示すエラーが返されますエラーメッセージは、ツールボックスウィンドウに表示されます。それ以上の行は読み込まれず、エラーが返されます。.最後に将来のための重要な情報を設定し、後で無駄な計算をしなくて済むようにします。

~C_Widget()
{
        Terminal.Close();
        ObjectsDeleteAll(Terminal.Get_ID(), def_PrefixName);
        ArrayFree(m_Infos.Symbols);
}

この関数は、クラスのデストラクタです。クラスが閉じられると自動的に呼び出されます。その場合、システム全体も一緒に終了し、クラス内で生成されたオブジェクトはすべて削除され割り当てられたメモリは解放されます

次のコードでは、クラスの初期化システムを用意しています。

bool Initilize(const string szFileConfig, const string szNameShort, color corText, color corPrice, color corBack)
{
        IndicatorSetString(INDICATOR_SHORTNAME, szNameShort);
        Terminal.Init(ChartWindowFind());
        Terminal.Resize();
        m_Infos.CorBackGround = corBack;
        m_Infos.CorPrice = corPrice;
        m_Infos.CorSymbol = corText;
        CreateBackGround();

        return LoadConfig(szFileConfig);
}

ここで使われているものは、以下の点以外はすでに説明済みなので、語るべきことはあまりありません。ここでは、インジケータの短い名前を定義していますこの名前は、パラメータとして通知されるので 、注意してください。さて、ここでのコードは、インジケータが使用するサブウィンドウのインデックスを取得するために使用されます。どのサブウィンドウが使われているのかがわからないと、間違った場所にオブジェクトを配置することになってしまいます。

そして、このヘッダーファイルの最後の関数として、メッセージングシステムがあります。

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        static int tx = 0;
        string szRet[];
                                                        
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_RollingTo):
                        tx = (int) (tx + lparam);
                        tx = (tx < -def_MaxWidth ? m_Infos.MaxPositionX : (tx > m_Infos.MaxPositionX ? -def_MaxWidth : tx));
                        for (int c0 = 0, px = tx; (c0 < m_Infos.nSymbols); c0++)
                        {
                                if (px < Terminal.GetWidth()) UpdateSymbolInfo(px, m_Infos.Symbols[c0].szCode);
                                px += def_MaxWidth;
                                px = (px > m_Infos.MaxPositionX ? -def_MaxWidth + (px - m_Infos.MaxPositionX) : px);
                        }
                        ChartRedraw();
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        Terminal.Resize();
                        m_Infos.MaxPositionX = macro_MaxPosition;
                        ChartRedraw();
                        break;
                case CHARTEVENT_OBJECT_DELETE:
                        if (StringSubstr(sparam, 0, StringLen(def_PrefixName)) == def_PrefixName) if (StringSplit(sparam, '#', szRet) == 2)
                        {
                                AddSymbolInfo(szRet[1], true);
                                ChartRedraw();
                        }else if (sparam == def_NameObjBackGround)
                        {
                                ObjectsDeleteAll(Terminal.Get_ID(), def_PrefixName);
                                CreateBackGround();
                                for (int c0 = 0; c0 < m_Infos.nSymbols; c0++) AddSymbolInfo(m_Infos.Symbols[c0].szCode, true);
                                ChartRedraw();
                        }
                        break;
        }
}

このコードのほとんどは非常にシンプルです。プラットフォームが生成した2つのイベントをインジケータに渡して処理していますが、カスタムイベントであるため、多くの人にとって意味のないイベントタイプも存在します。このタイプのイベントは、ある種のプロジェクトではよく見られるものですが、ここでは、発生しうるメッセージやイベントの処理を一元化する役割を担っています。理解していらっしゃらない方が多いのですが、MetaTrader 5プラットフォームとMQL5言語はイベント指向です。つまり、手続き的に動くのではなく、イベントと連携し、イベントが発生したときに処理するのです。

カスタムイベントがどのように生成されるかを理解するためには、インジケータのコードを見る必要があります。そのため、説明の前に(このイベントを正確に理解することが困難な方も多いと思いますが)、冒頭とは異なる機能的な見方をするようになったインジケータのコードをご覧ください。

#property copyright "Daniel Jose"
#property description "Program for a panel of quotes."
#property description "It creates a band that displays asset prices."
#property description "For details on how to use it visit:\n"
#property description "https://www.mql5.com/ru/articles/10941"
#property link "https://www.mql5.com/ru/articles/10941"
#property indicator_separate_window
#property indicator_plots 0
#property indicator_height 45
//+------------------------------------------------------------------+
#include <Widget\Rolling Price\C_Widget.mqh>
//+------------------------------------------------------------------+
input string    user00 = "Config.cfg";  //Configuration file
input int       user01 = -1;            //Shift
input int       user02 = 60;            //Pause in milliseconds
input color     user03 = clrWhiteSmoke; //Asset color
input color     user04 = clrYellow;     //Price color
input color     user05 = clrBlack;      //Background color
//+------------------------------------------------------------------+
C_Widget Widget;
//+------------------------------------------------------------------+
int OnInit()
{
        if (!Widget.Initilize(user00, "Widget Price", user03, user04, user05))
                return INIT_FAILED;
        EventSetMillisecondTimer(user02);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+
void OnTimer()
{
        EventChartCustom(Terminal.Get_ID(), C_Widget::Ev_RollingTo, user01, 0.0, "");
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Widget.DispatchMessage(id, lparam, dparam, sparam);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        EventKillTimer();
}
//+------------------------------------------------------------------+

これは、インジケータコードではほとんど使われていないもので、インジケータウィンドウの高さを指定するものです。これは大事ではありません。以下の詳細にご注目ください。

ユーザーがこのパラメータの値を定義すると、タイマーとして使用するデータを取得することができます。可能であれば、インジケーターでOnTimeイベントを使用することを避けるべきであるというのは事実ですが、ここでは残念ながら、それ以外の選択肢がありません。このイベントは必要です。MetaTrader 5プラットフォームがOnTimeイベントをトリガーするとき、OnTimeイベントコールが生成されることに注意してください。この関数の内部には、非同期イベントを起動する1行だけがあります。つまり、このコードがいつ呼び出されるかはわからないということです。これはカスタムイベントです。

カスタムイベント内のパラメータは、カジュアルなものではないことに注意してください。これらは非常に重要な理由でそこに配置されています。それぞれが1つのことを示しますが、最終的には OnChartEventの呼び出しがあり、イベントによって生成されたメッセージを処理するC_Widgetクラス内の関数が呼び出されるのです。

次に、以下にご注目ください。EventChartCustom関数を使用する場合、OnChartEvent関数のIDとして使用されるイベントを設定します。この値は、メッセージ処理機能で特定されます。メッセージ処理関数を直接呼び出したとすると、コードは非同期になるので、メッセージ処理関数の返事を待つために、残りのコードを待機モードに設定することになります。しかし、EventChartCustomの呼び出しを使用しているので、コードは待機状態にはなりません。これにより、期間がわからないもので他のインジケータをブロックすることを避けることができます。

EventChartCustomを介して呼び出しを実装することにはもう1つの利点があります。この呼び出しは、コードのどのポイントからでも可能だということです。どこから呼び出しても、ChartEventは常にトリガーされ、OnChartEventを呼び出して必要な実行を確保します。

この方法は、別の興味深いトピックに関する別の記事でも使用されます。記事がリリースされる前に興味をそそるために、今はそれについて話しません。

カスタムイベントを生成する方法と、パネルを動かすコードを直接呼び出す代わりにカスタムイベントを使用する理由はが明らかになったかと思います。さて、このカスタムパネルの移動イベントの処理を含むコードに戻りましょう。 ユーザーによって指定されるパラメータがあり、これは移動にとって非常に重要です。.

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        static int tx = 0;
        string szRet[];
                                                        
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_RollingTo):
                        tx = (int) (tx + lparam);
                        tx = (tx < -def_MaxWidth ? m_Infos.MaxPositionX : (tx > m_Infos.MaxPositionX ? -def_MaxWidth : tx));
                        for (int c0 = 0, px = tx; (c0 < m_Infos.nSymbols); c0++)
                        {
                                if (px < Terminal.GetWidth()) UpdateSymbolInfo(px, m_Infos.Symbols[c0].szCode);
                                px += def_MaxWidth;
                                px = (px > m_Infos.MaxPositionX ? -def_MaxWidth + (px - m_Infos.MaxPositionX) : px);
                        }
                        ChartRedraw();
                        break;

上のコードに含まれる数学は分かりにくいかもしれませんが、ここでやっているのは、ユーザーから提供された値を使って、オブジェクトを一定の距離だけ移動させることです。値が正の場合は左から右へ、負の場合は右から左へ移動し、0の場合はそのままです。発想はシンプルですが、目に見えない計算はどこにあるのでしょうか。上のコードは分かりにくいと言ったのはこのためです。この2行で計算がおこなわれます。

どうしてそんなことが可能なのか、どうしてこんな単純な計算でこんなことができるのか、十分理解できないかもしれません。しかし、注意深く見ていると、私たちが制限を使っていることがわかります。上限値に達すると、そのすぐ隣の限界まで位置が再計算されます。ある値に到達しようとするときに、逆の限界点からスタートするように調整するループを閉じるのです。わかりやすく言うと、0から99まで数えて、それ以上は数えられないとしたら、99に1を足そうとしたらどうなるのか、ということです。論理的には100になるはずです。

しかし、この場合は違います。0に戻ることになります。98に3を足そうとしても、99以上にはならず、1になってしまうのです。不思議な感じですが、こういうことになります。2から3を引くと99になるのも同じです.。おかしいです😵 😵 😵... ...が、これがコンピュータのカウントシステムの基本です。勉強すれば、コンピュータが無限大の数まで計算しているわけではないことがわかります。暗号化という別の分野で適用される最大取得値には一定の限界がありますが、これはまた別の話です。 

コードに戻りましょう。FORループの話になると、さらにおかしなことになるので、今話したことを理解するようにした方がいいでしょう。

FORループの中で次のようなことをしています。上記の計算では、画面のどこに何を表示すべきかが分からないので、どこでどの程度終了させればいいのかが分からないのです。そのためには、ウィンドウを作成する必要があります。むしろ、チャートウィンドウの限界を利用して、何を表示すべきか、何を表示すべきでないかを知ることになります

この部分は、上記の概念を理解していないと、非常に分かりにくいでしょう。ある情報は、「いくつの要素を表示するか」「現在どの値が使われているか」の2つだけです。この情報をもとに、あとは全部やってしまいましょう。常にゼロの要素から出発して、各要素の幅を開始位置に足していきながら、要素から要素へと進んでいこうと思いますある時点で、上限バンドか下限バンドのどちらかを超えてしまうのです。この値を超えると、現在の要素がプロットされる場所を示すために使用している値は、それに応じて調整される必要があります。一旦これが起こると、位置が外れ、情報が魔法のように画面の一方の側に消えて、もう一方の側に現れ始めるでしょう

このサイクルは、インジケータが閉じるまで繰り返されるので、どんなに情報が多くても、すべての情報が画面に表示されます。

これは、純粋なテキストでおこなう方がはるかに簡単であり、計画も立てやすいのです。しかし、手法はよく似ているものの、通常、ほとんどの人が使っているのは、行列を使ったコードです。その中で要素が動き、各セルの表示位置はすでにきちんと決まっています。しかし、これはここでは望ましい結果をもたらさないので、純粋に数学的アプローチを使用して滑らかで正しい動きを生成する別の方法を使用する必要がありました。

後1つ細かいことを言うと、-1や1より大きい値を使うと、動きが半パルスになってしまい、変な印象を与えてしまうので、避けた方がいいです。

以下のビデオでは、IBOV (Ibovespa Index)資産のデータを使って、このシステムが実際に動いている様子をご覧いただけます。これはあくまでシステムの仕組みを示すものです。




結論

このシステムは完全に完成したように見えますが、まだ改良の余地があります。次回は、このようなシステムを改善する方法を紹介します。今後もアップデートしていくので、ご期待ください。添付ファイルには、本記事の全コードが含まれています。お好きなようにお使いください。


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

添付されたファイル |
DoEasy - コントロール(第31部):ScrollBarコントロールのコンテンツのスクロール DoEasy - コントロール(第31部):ScrollBarコントロールのコンテンツのスクロール
この記事では、水平スクロールバーのボタンを使用してコンテナのコンテンツをスクロールする機能を実装します。
ティッカーテープパネルの作成:改良版 ティッカーテープパネルの作成:改良版
ティッカーテープパネルの基本バージョンを復活させるというアイデアはいかがでしょうか。まずおこなうのは、資産のロゴやその他の画像などの画像を追加できるようにパネルを変更して、ユーザーが表示された銘柄をすばやく簡単に識別できるようにすることです。
自動で動くEAを作る(第01回):概念と構造 自動で動くEAを作る(第01回):概念と構造
今日は、自動モードでシンプルかつ安全に動作するエキスパートアドバイザー(EA)を作成する方法を紹介します。
母集団最適化アルゴリズム:ホタルアルゴリズム(FA) 母集団最適化アルゴリズム:ホタルアルゴリズム(FA)
今回は、ホタルアルゴリズム(FA)という最適化手法について考えてみます。修正により、このアルゴリズムは部外者から真の評価表リーダーへと変貌を遂げました。