English Русский 中文 Español Deutsch Português
preview
自動で動くEAを作る(第05回):手動トリガー(II)

自動で動くEAを作る(第05回):手動トリガー(II)

MetaTrader 5トレーディング | 20 3月 2023, 09:37
893 0
Daniel Jose
Daniel Jose

はじめに

自動で動くEAを作る(第04回)手動トリガー(I)」と題した前回の記事では、ちょっとしたプログラミングで、キーとマウスの組み合わせで成行注文を出したり指値注文を出したりする方法を紹介しました。

前回の最後に、少なくともしばらくはEAを手動で使えるようにするのが適切ではないかと提案しました。当初は、自動売買ができるEAの開発を実際に始める方法を紹介する記事を3~4本公開する予定だったのですが、これが予想以上に面白いことになりました。プログラマーにとってはかなり簡単なことですが、あるものを実際にどのようにプログラミングするのかを明確に説明した資料がほとんどないため、プログラミングを学び始めたばかりの初心者にとっては、難しいことかもしれません。さらに、知識のレベルを1つに限定することは、本当はやってはいけないことなのです。

このコミュニティで公開されている記事を参考にプログラミングの勉強を始める方も多いと思いますので、C/C++で長年プログラミングをしてきた私の経験を共有し、C/C++と非常に似ているMQL5でいくつかのことを実装する方法を紹介する機会だと考えています。プログラミングは神話的なものではなく、すべて現実のものであることを示したいのです。

さて、手動モードのEAをより快適に使うためには、いくつかの工夫が必要です。この作業はプログラマーにとってシンプルで簡単なものなので、本題に入ることができます。すなわち、取引サーバに送る注文の注文の制限の位置を示す行を作成するのです。

これらの制限は、マウスを使って注文を出すとき、つまり指値注文を作成するときに確認できるのがより適切です。注文がすでにサーバ上にある場合、その表示はMetaTrader 5プラットフォームによって管理されます。しかし、実際にそうなる前に、注文制限を配置する可能性が高い場所をユーザーに示す必要があり、これは、私たちプログラマーがおこないます。MetaTrader 5から受けられるサポートは、チャート上で水平線を使用できることのみです。それ以外は、すべてEAプログラミングで実装する必要があります。

そのためには、この線をチャートの正しい位置に配置するコードを書けばよいのです。でも、適当にやるべきではありません。すでに作成したコードを損ないたくないし、将来的にC_MouseクラスとOnChartEventイベントハンドラをEAから削除しなければならない場合にば作業を増やしたくないので、これは適切に制御する必要があります。自動EAにはこれらは必要ないが、手動EAには必要だからです。こういうものは、最低限使えるようにしないといけません。 


C_Terminalクラスの作成

そのために、手作業から便利なものを作っていきます。送信される注文の可能な制限やポジションを示す行を追加する必要があります。その際、C_OrdersクラスとC_Mouseクラスで重複するコードを削除することになります。新しいC_Terminalクラスを用意することで、快適に作業できるように、いくつかのものを構築し、分離することができるようになります。このクラスを使うことで、今後、自動EAも手動EAも、新しいEAに何らかの致命的な障害を発生させるリスクを負うことなく、作成できるようになるのです。

最大の問題は、新しい自動売買EAは、多くの場合、ゼロから作られるということです。この方法では、チェックが十分でないため、多くのエラーが発生することが多くなります。

確かに、これらのクラスを非公開ライブラリに転用するのはなかなか面白いかもしれませんが、モードによって意図が違うので、今は考えないことにします。もしかしたら、今後おこなうかもしれません。実際に何をするのかを確認しましょう。まずは以下のように、いつものようにC_Terminal.mqhというヘッダーファイルを作成します。これは、これから作るクラスには必ず存在する、最も基本的なコードです。以下に示します。

class C_Terminal
{
        private :
        public  :
};

ある点はprivateセクションに、ある点はpublicセクションに置くことができることを決して忘れないように、クラス内のコードを常に初期化します。クラスにprivateなものがなくても、物事を明確にすることは常に大切です。主に、自分のコードを他の人に見せることができるからです。

区切りの良く、よく書いてあるコード、つまり読みやすいコードは、他の人の興味を引き、問題のトラブルシューティングに困ったときに解析することを促すはずです。整理されておらず、タブもなく、説明のコメントもないようなコードは、興味をひかなくなります。たとえアイデアが良くても、他人のコードが何をしているのかを理解するために整理するのに時間をかけるのは、誰も好きではありません。

それが私のアドバイスです。もちろん、私自身のコードは完璧ではありませんが、常にコードをきれいに保ち、1つのプロシージャの中に入れ子になっている複数の行を書く必要があるときはいつでもタブを使用することは、非常に役立ちます。他人だけでなく、ほとんどが自分です。コードには、作成者でも理解できないようなひどいものがあります。他のプログラマーはどうするのでしょうか。

では、さっそくコードを書いて、クラスに構造体を追加してみましょう。コードの最初の行を以下に示します。

class C_Terminal
{
        protected:
//+------------------------------------------------------------------+
                struct stTerminal
                {
                        ENUM_SYMBOL_CHART_MODE ChartMode;
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                };
//+------------------------------------------------------------------+
        private :
        public  :
};

ここでは、新しいことを紹介します。予約語のprotectedです。しかし、それで何が分かるのでしょうか。普段はprivate宣言とpublic宣言しか使っていませんでした。では、これは何でしょう。実は、これはpublicなものとprivateの中間です。何が起こっているのかを理解するためには、オブジェクト指向プログラミングの基本的な概念を理解する必要があります。

その概念の1つが継承です。ただし、継承の話題を掘り下げる前に、個人を例にしてクラスを考えてみましょう。そこで、この概念をよりよく理解するために、各クラスを、個々人の、ユニークで排他的な生き物のものとして考えてみてください。では、解説に進みます。

情報の中には、それを保持する個人に加え、誰もがその使用や知識から利益を得ることができる、公開されたものがあります。この情報は、常にコードのpublic部分に置かれます。その他の情報は、個人のプライベートなデータ、つまり、その個人しかアクセスできないものです。その人がいなくなれば、その情報も一緒に消えてしまう。そして、その情報を享受できるのは、その人だけです。情報は個人のスキルだと思って大丈夫です。その人は他の人に教えたり、伝えたりすることはできないし、誰も奪うことはできません。この種の情報は、コードのprivate部分にあります。

しかし、いずれの概念にも当てはまらない情報も存在します。これは、protected部分にあって、本人が使っても使わなくてもよいものです。さらに血筋を受け継いでいくことができるのが大きな特徴です。この仕組みを理解するために、継承の話題を掘り下げてみましょう。

継承の話になると、一番わかりやすいのは、血統を考えることです。継承には、public、private、protestの3種類があります。ここで私が言っているのは継承の話であって、血筋の各メンバーの個別の問題ではありません。

public継承では、親の情報、データ、コンテンツが子や孫以降を含むすべての子孫に引き継がれ、血縁者以外の人も理論上はこれらにアクセスすることができます。理論上というフレーズに注目してみてください。このような転送にはニュアンスがあります。これについては、後ほど詳しく見ていきます。まずは継承に着目してみましょう。private継承については、初代だけが情報にアクセスでき、それ以降の世代はたとえ血筋であってもアクセスすることはできません。

そして、最後にあるのがprotected継承です。private継承とよく似たものが生まれます。しかし、このような概念を多くの人に理解させない悪化要因として、「親条項」があります。というのも、public継承の場合であっても、情報の受け渡しには一種のルールがあるのです。血統の外ではアクセスできないものがあります。これを理解するために、この問題を簡単にまとめた下の表をご覧ください。

親クラスでの定義 継承の種類 子クラスからのアクセス 子クラスの呼び出しによるアクセス 
private public アクセスが却下される ベースクラスのデータまたはプロシージャにアクセスできない
public public アクセスが許可される ベースクラスのデータまたはプロシージャにアクセスできる
protected public アクセスが許可される ベースクラスのデータまたはプロシージャにアクセスできない
private private アクセスが却下される ベースクラスのデータまたはプロシージャにアクセスできない
public private アクセスが許可される ベースクラスのデータまたはプロシージャにアクセスできない
protected private アクセスが許可される ベースクラスのデータまたはプロシージャにアクセスできない
private protected アクセスが却下される ベースクラスのデータまたはプロシージャにアクセスできない
public protected アクセスが許可される  ベースクラスのデータまたはプロシージャにアクセスできない
protected protected アクセスが許可される ベースクラスのデータまたはプロシージャにアクセスできない

表1:情報定義に基づく継承システム

なお、継承時のデータ型定義で使用されるコード部分によって、子がそのデータにアクセスできる場合とできない場合があります。ただ、親のデータがpublicに宣言され、子も同じようにpublicに継承する場合に起こるユニークなケースを除き、血統外の呼び出しはアクセスできません。また、系統以外の情報には一切アクセスできません。

経験の浅いプログラマーの多くは、表01に示すスキームを理解しないまま、オブジェクト指向プログラミングを軽視しています。実は物事の本当の仕組みを知らないからです。私の記事や私のコードをご覧になっている方は、私がオブジェクト指向プログラミングを多用していることにお気づきでしょう。

そうでなければ不可能な非常に複雑なものを実装する際に、究極の安全性を提供するためです。これは継承に限られたことではありません。そのほか、ポリモーフィズムやカプセル化などもありますが、それはまた別の機会に紹介します。カプセル化は表01の一部ですが、より詳細な説明が必要であり、この記事の範囲外です。

続けましょう。よく見ると、上のコードの構造は、C_Ordersクラスと同じであることに気づきます。C_Orderクラスはこのデータの定義を失い、C_Terminalクラスからデータを継承するようになるため、この点に注意してください。ただし、今のところはC_Terminalクラスの内部にとどまることにしましょう。

次にC_Terminalクラスに追加するのは、C_MouseクラスとC_Ordersクラスの両方に共通する関数です。これらの関数はC_Terminalクラスのprotected部分に追加されるため、C_TerminalからC_MouseクラスとC_Ordersクラスを継承した場合、これらの関数と手続きは表01に従うことになります。そこで、以下のコードを追加します。

//+------------------------------------------------------------------+
inline double AdjustPrice(const double value)
                        {
                                return MathRound(value / m_TerminalInfo.PointPerTick) * m_TerminalInfo.PointPerTick;
                        }
//+------------------------------------------------------------------+
inline double FinanceToPoints(const double Finance, const uint Leverage)
                        {
                                double volume = m_TerminalInfo.VolMinimal + (m_TerminalInfo.VolStep * (Leverage - 1));
                                
                                return AdjustPrice(MathAbs(((Finance / volume) / m_TerminalInfo.AdjustToTrade)));
                        };
//+------------------------------------------------------------------+

これらのコードは、両クラスで重複することはなくなりました。現在、すべてのコードはC_Terminalクラス内にのみ存在し、メンテナンス、テスト、および可能な修正を容易にしています。このように、私たちのコードは、使用したり拡張したりするにつれて、より信頼性が高く、魅力的なものになります。

C_Terminalクラスの内部では、さらにいくつかの注意すべき点があります。その前に、クラスのコンストラクタを見てみましょう。以下に示します。

        public  :
//+------------------------------------------------------------------+
                C_Terminal()
                        {
                                m_TerminalInfo.nDigits          = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
                                m_TerminalInfo.VolMinimal       = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                                m_TerminalInfo.VolStep          = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
                                m_TerminalInfo.PointPerTick     = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                                m_TerminalInfo.ValuePerPoint    = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
                                m_TerminalInfo.AdjustToTrade    = m_TerminalInfo.ValuePerPoint / m_TerminalInfo.PointPerTick;
                                m_TerminalInfo.ChartMode        = (ENUM_SYMBOL_CHART_MODE) SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE);
                        }
//+------------------------------------------------------------------+

なお、C_Ordersクラスに存在したものとほぼ同じです。ここで、C_Ordersクラスのコードを修正して、C_Terminalクラスで実装しているものを継承するようにします。しかし、1つだけ大切なことがあります。上記コンストラクタで初期化される構造体の宣言を含むコード部分をご覧ください。変数がないことがわかります。なぜでしょうか。

理由は、カプセル化です。クラス外のコードがクラス内部変数の内容にアクセスし、変更することを許可してはいけません。これは重大なプログラムエラーです。コンパイラは文句を言いませんが、絶対に許可してはいけません。クラスのグローバル変数は、必ずprivate部分で宣言する必要があります。変数宣言は以下のようになります。

//+------------------------------------------------------------------+
        private :
                stTerminal m_TerminalInfo;
        public  :
//+------------------------------------------------------------------+

なお、グローバルクラス変数は、private部分とpublic部分の間に定義されます。したがって、C_Terminalから継承するクラスでは使用できません。つまり、情報のカプセル化を保証すると同時に、コードに継承を追加します。コードの実用性を高めるだけでなく、堅牢性のレベルも指数関数的に高めています。

しかし、上記のクラスで必要なデータにどのようにアクセスするのでしょうか。親クラス(この場合はC_Terminalクラス)の変数にある程度アクセスできるようにする必要があります。そう、これが必要なのですが、これは、これらの変数をpublicやprotectedにすることでおこなうべきではありません。これはプログラムエラーです。派生クラスが親クラスの値にアクセスできるように、何らかの手段を追加する必要があります。しかし、ここに重要な危険が潜んでいます。派生クラスによる親クラスの変数の変更を許可してはなりません。 

そのためには、何らかの方法で変数を定数に変換する必要があります。つまり、親クラスは必要なときに必要なだけ、変数の値を変更することができます。子クラスが親クラスの変数に何らかの変更を加えたい場合、子クラスは親クラスが提供する何らかの手続きを呼び出し、親クラスに存在する特定の変数に使用する希望値を通知しなければなりません。このような手続きは、親クラスに実装されるべきもので、子クラスから渡されたデータが有効かどうかを確認することになります。もしそれが有効であれば、プロシージャは子から要求された変更を親クラス内にプッシュします。

しかし、親クラスに知られることなく、子要素が親データを変更することは絶対にありえません。そのような危険なコードもたくさん見てきました。子から提供されたデータを確認するために親クラス内のプロシージャを呼び出すと、コードが遅くなるとか、実行時にプログラムがクラッシュするとかいう意見もあります。しかし、これはエラーです。親クラスの内部で不正な値を投げるコストとリスクは、データチェック手続きの呼び出しを省略することで得られるわずかなスピードアップに見合うものではありません。ですから、これでコードが遅くなるのではと心配する必要はありません。

さて、もう1つのポイントにたどり着きました。プログラマーとしてプロフェッショナルを目指すのであれば、他のクラスに継承されるプロシージャは必ずprotectedコードパートに配置することを優先し、最後の手段としてプロシージャをpublicエリアに移動させる必要があります。常にカプセル化を優先します。本当に必要な場合のみ、カプセル化を解除し、関数やプロシージャの一般利用を許可します。 しかし、変数は常にprivateであるべきなので、変数でそれをおこなうことはありません。

子クラスが親クラスのデータにアクセスするためのプロシージャや関数を作成するには、次の関数を使用します。

inline const stTerminal GetTerminalInfos(void) const
                        {
                                return m_TerminalInfo;
                        }

これから説明することは非常に重要で、よくできたコードと単によくできたコードの違いを生むので、よく注意してください。

この記事を通して、変数が宣言され使用されているクラスの外のコードが、何らかの方法で変数にアクセスできるようにしなければならないと述べてきました。理想的なのは、宣言されたクラスの中で、必要なときにいつでも変数を変更できる状況だと言いましたが、クラスの外では、変数は定数として扱われるべきで、つまりその値を変更することはできません。

極めてシンプルであるにもかかわらず、上記のコードはまさにこれを実現しています。つまり、C_Terminalクラスの内部ではアクセス可能な変数があり、その値を変更することができるが、クラスの外部では同じ変数が定数として扱われることを保証することができるのです。どのようにおこなうのか、なぜここに2つの予約語constがあるのでしょうか。

1つずつ考えてみましょう。最初の単語constは、戻り値の変数m_TerminalInfoを呼び出し元の関数で定数とみなすようコンパイラに通知します。 呼び出し元が、返された変数に示された構造体メンバーの値を変更しようとした場合、コンパイラはエラーを発生させ、そのコエドをコンパイルすることができなくなります。2番目の単語constは何らかの理由でここで何らかの値が変更された場合、コンパイラはエラーを返すべきであると通知しています。したがって、この関数は値を返すためだけに存在するため、この関数内のデータを変更しようと思っても、変更することはできません。

プログラマーの中には、変数が外部からのアクセスにのみ使用されるべきで、ある種の因数分解には使用されないような関数やプロシージャの中で変数の値を変更する間違いを犯す人がいます。上記のコード原則を使用することで、このようなエラーを回避することができます。

また基となるC_Terminalクラスは完成していませんが、コードの重複部分を削除することで、C_MouseクラスはC_Ordersクラスと同じ種類のコードを持つようになります。ただし、C_Mouseクラスを変更するのははるかに簡単なので、C_Terminalクラスを継承することで、どのようになるかを見てみましょう。これは、以下のコードで確認できます。

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Terminal.mqh"
//+------------------------------------------------------------------+
#define def_MouseName "MOUSE_H"
//+------------------------------------------------------------------+
#define def_BtnLeftClick(A)     ((A & 0x01) == 0x01)
#define def_SHIFT_Press(A)      ((A & 0x04) == 0x04)
#define def_CTRL_Press(A)       ((A & 0x08) == 0x08)
//+------------------------------------------------------------------+
class C_Mouse : private C_Terminal
{
// Inner class code ....
};

ここでは、C_Terminalクラスのヘッダーファイルをインクルードしています。ファイル名を二重引用符で囲んでいることに留意してください.。これは、C_Terminal.mqhファイルがC_Mouse.mqhファイルと同じディレクトリにあることをコンパイラに伝えます。こうすることで、両方のファイルを別の場所に移動する必要があった場合、コンパイラは、それらが同じディレクトリにあることを知っているので、常に正しいファイルを見つけることができるようになります。

さて、常に最小限のアクセスで作業を開始するという考え方に則り、C_MouseクラスをC_Terminalクラスのprivateな子供にしましょう。これで、C_MouseクラスからAdjustPrice関数を削除し、C_Mouseに存在するPointPerTickクラスも、今度はC_Terminalクラスのプロシージャを使用するので、削除することができます。クラスはprivateに継承され、AdjustPrice関数はコードのprotected部分の中にあるので、C_Terminalには表01のような結果が表示されます。したがって、先ほどのようにC_Mouseクラスの外でAdjustPriceプロシージャを呼び出すことは不可能になります。

ただし、C_Mouseクラスのこれらの変更はすべて一時的なものです。EAを手動で使うときに必要な制限ラインを追加するために、もう少し変更します。しかし、それは後回しにします。C_Ordersクラスで、より深い変更を加える方法を見てみましょう。これらの変更については、別のトピックが必要です。では、次に進みましょう。


C_Terminalからの継承後にC_Ordersクラスを変更する

C_Mouseクラスと同様に変更を開始します。下のコードにあるような違いが出てきます。

#include "C_Terminal.mqh"
//+------------------------------------------------------------------+
class C_Orders : private C_Terminal
{
        private :
//+------------------------------------------------------------------+
                MqlTradeRequest m_TradeRequest;
                ulong           m_MagicNumber;
                struct st00
                {
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                        bool    PlotLast;
                        ulong   MagicNumber;
                }m_Infos;
//+------------------------------------------------------------------+

全体の原理はC_Mouseクラスとほぼ同じですが、いくつか異なる点があります。まず、強調表示された行に示すように、C_Ordersクラス構造体を削除します。ただし、構造体内部のあるデータが必要なので、privateにするが、通常の変数としてです

強調表示されている部分を削除するので、新しいコードを書くのに手間がかかると思われるかもしれません。実は、かなり大変なことになりそうです。早速、このC_Ordersクラスのコンストラクタに移りましょう。変更は、実はここから始まるのです。以下は、新しいクラスのコンストラクタです。

                C_Orders(const ulong magic)
                        :C_Terminal(), m_MagicNumber(magic)
                        {
                                m_Infos.MagicNumber     = magic;
                                m_Infos.nDigits         = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
                                m_Infos.VolMinimal      = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                                m_Infos.VolStep         = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
                                m_Infos.PointPerTick    = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                                m_Infos.ValuePerPoint   = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
                                m_Infos.AdjustToTrade   = m_Infos.ValuePerPoint / m_Infos.PointPerTick;
                                m_Infos.PlotLast        = (SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_LAST);
                        };

ご覧のように、コンストラクタの内部内容はすべて削除されていますが、ここではC_Terminalクラスのコンストラクタを強制的に呼び出しています。 これは、他の何よりも先に呼び出されるようにするためのものです。通常はコンパイラがやってくれますが、ここでは明示的に実装し、同時にコード内の別の場所でマジックナンバーを示す変数を初期化することにします。 

これは普通はコンストラクタでおこなわれます。コードが実行される前に変数の値を定義しておき、コンパイラが適切なコードを生成できるようにするのが理由ですが、通常のように値が一定であれば、このようにすることで、C_Ordersクラスの初期化の時間を短縮することができます。ただし、以下の詳細を覚えておいてください。値が定数である場合にのみ何らかの恩恵を受けることができ、そうでない場合はコンパイラが実用的な恩恵を与えないコードを生成してしまいます。

次に、C_OrdersクラスからAdjustPrice関数とFinanceToPoints関数を削除しますが、これは直接できるので、ここでは紹介しません。今後、これらの呼び出しにはC_Terminalクラス内のコードを使用することになります。

C_Terminalクラスで宣言された変数を使用するコードの1部分を見てみましょう。親クラスの変数にアクセスする方法を理解するのが目的です。次のコードをご覧ください。

inline void CommonData(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double Desloc;
                                
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.magic            = m_Infos.MagicNumber;
                                m_TradeRequest.magic            = m_MagicNumber;
                                m_TradeRequest.symbol           = _Symbol;
                                m_TradeRequest.volume           = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - 1)), m_Infos.nDigits);
                                m_TradeRequest.volume           = NormalizeDouble(GetTerminalInfos().VolMinimal + (GetTerminalInfos().VolStep * (Leverage - 1)), GetTerminalInfos().nDigits);
                                m_TradeRequest.price            = NormalizeDouble(Price, m_Infos.nDigits);
                                m_TradeRequest.price            = NormalizeDouble(Price, GetTerminalInfos().nDigits);
                                Desloc = FinanceToPoints(FinanceStop, Leverage);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), m_Infos.nDigits);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), GetTerminalInfos().nDigits);
                                Desloc = FinanceToPoints(FinanceTake, Leverage);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), m_Infos.nDigits);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), GetTerminalInfos().nDigits);
                                m_TradeRequest.type_time        = (IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
                                m_TradeRequest.stoplimit        = 0;
                                m_TradeRequest.expiration       = 0;
                                m_TradeRequest.type_filling     = ORDER_FILLING_RETURN;
                                m_TradeRequest.deviation        = 1000;
                                m_TradeRequest.comment          = "Order Generated by Experts Advisor.";
                        }

強調表示された部分は削除され、他の強調表示されたコードに置き換えられています。では、黄色で強調表示されたされているコードに注目してください。 多くの人が見たことがないようなものが入っています。なお、これらの黄色で強調表示されたコードには、構造体としてみなされる関数が存在します。しかし、とんでもないことになってます😵😱

皆さん落ち着いてください。ご安心ください。とんでもなくはないのです。今まで見たことのないような、ちょっとエキゾチックな方法でプログラミングしているだけです。なぜこれが許されるのか、なぜ機能するのかを理解するために、関数を分けて見てみましょう。

GetTerminalInfos().nDigits

では、C_Terminalクラスのコードに戻って、この関数がどのように宣言されているかをご覧ください。以下のようになります。

inline const stTerminal GetTerminalInfos(void) const
                        {
                                return m_TerminalInfo;
                        }

以下のコードに示すように、GetTerminalInfos関数は構造体を返します

                struct stTerminal
                {
                        ENUM_SYMBOL_CHART_MODE ChartMode;
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                };

コンパイラにとって、GetTerminalInfos().nDigitsのコードを使ってやっていることは、GetTerminalInfos()が関数ではなく、変数😲だと言うことと同じになります。おわかりでしょうか。コンパイラにとって、GetTerminalInfos().nDigitsのコードは、次のコードと等価であるため、事態はさらに興味深いものになります。

stTerminal info;
int value = info.nDigits;

value = 10;
info.nDigits = value;


つまり、値を読むだけでなく、書き込むこともできるのです。そこで、誤って次のような断片を書き込んでしまったとします。

GetTerminalInfos().nDigits = 10;

コンパイラは、関数GetTerminalInfos()によって参照される変数に値10を配置する必要があることを認識します。 この場合、参照される変数はC_Terminalクラスにあり、この変数はコードのprivate部分で宣言されているため、問題となります。つまり、先におこなった呼び出しで変更できないません。ただし、GetTerminalInfos()関数もprotectedなので(publicでも同じですが)、privateと宣言された変数は、それを参照する関数と同じアクセスレベルになってしまいます。

これらがいかに危険なものであるか、おわかりいただけたでしょうか。つまり、ある変数をprivateと宣言しても、それを参照する関数やプロシージャのコードを間違って書くと、自分や他人がその値を不用意に変えてしまう可能性があります。そして、カプセル化という概念を壊してしまいます。

しかし、関数の宣言時にキーワードconstで起動したためにコンパイラによるGetTerminalInfo()関数の見方が変わり、これですべてが変わりました。このことを理解するために、C_Ordersクラス内の任意の場所で以下のコードを使用してみてください。

GetTerminalInfos().nDigits = 10;

これを実行しようとすると、コンパイラはエラーをスローします。なぜなら、コンパイラはGetTerminalInfos().nDigitsGetTerminalInfos()が参照する構造体内のものを定数とみなし、定数の値を変更することができないからです。これはエラーとみなされます。

これで、変数を使った定数データの参照方法をおわかりでしょうか。C_TerminalクラスではGetTerminalInfos()関数が参照する構造体は変数ですが、それ以外のコードでは定数😁となります。

この部分を説明したところで、他の修正に移りましょう。これで、何が起きているのか、C_Ordersが参照するデータはどこから来ているのか、ご理解いただけたと思います。次に変更する関数は、すぐ下に表示されています。

                ulong CreateOrder(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double  bid, ask;
                                
                                bid = SymbolInfoDouble(_Symbol, (m_Infos.PlotLast ? SYMBOL_LAST : SYMBOL_BID));
                                bid = SymbolInfoDouble(_Symbol, (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? SYMBOL_LAST : SYMBOL_BID));
                                ask = (m_Infos.PlotLast ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                                ask = (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                                CommonData(type, AdjustPrice(Price), FinanceStop, FinanceTake, Leverage, IsDayTrade);
                                m_TradeRequest.action   = TRADE_ACTION_PENDING;
                                m_TradeRequest.type     = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : 
                                                                                    (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));                              
                                
                                return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : 0);
                        };

そして、最後に修正されるのは次です。

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
                                        m_TradeRequest.action   = (Price > 0 ? TRADE_ACTION_MODIFY : TRADE_ACTION_REMOVE);
                                        m_TradeRequest.order    = ticket;
                                        if (Price > 0)
                                        {
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), m_Infos.nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), GetTerminalInfos().nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), GetTerminalInfos().nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), GetTerminalInfos().nDigits);
                                                m_TradeRequest.type_time  = (ENUM_ORDER_TYPE_TIME)OrderGetInteger(ORDER_TYPE_TIME) ;
                                                m_TradeRequest.expiration = 0;
                                        }
                                }else if (PositionSelectByTicket(ticket))
                                {
                                        m_TradeRequest.action   = TRADE_ACTION_SLTP;
                                        m_TradeRequest.position = ticket;
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), GetTerminalInfos().nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), GetTerminalInfos().nDigits);
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

この部分はこれで終了です。コードの安定性はそのままにして、堅牢性を向上させました。以前は関数が重複していたため、あるクラスでは変更され、別のクラスでは同じままというリスクがありました。何らかのエラーが発生した場合、それを修正するのは簡単ではありません。一方のクラスで修正しても、もう一方のクラスにはそのエラーが残ってしまうため、コードの堅牢性や信頼性が低くなります。

常にこれを考えてください。安定性や堅牢性の面でコードを改善するためのちょっとした作業は、仕事ではなく、ただの趣味です😁。

しかし、この記事でやろうとしていたことは、まだ終わっていません。指値(テイクプロフィットやストップロスのレベル)を追加して、手動で注文するときにそのレベルを知ることができるようにしたいと思ったのですが、この部分がまだありません。それがなければ、この記事を終えて、次の記事に移ることはできません。しかし、このトピックをすでに取り上げたものから切り離すために、ここに新たなセクションを設けることにします。


テイクプロフィットラインとストップロスラインの作成

では、次の質問について考えてみましょう。これらの行を作成するコードを追加するのに最適な場所はどこでしょうか。呼び出し点を設けています。以下に示します。

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        uint            BtnStatus;
        double  Price;
        static double mem = 0;
        
        (*mouse).DispatchMessage(id, lparam, dparam, sparam);
        (*mouse).GetStatus(Price, BtnStatus);
        if (TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL))
        {
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP))  (*manager).ToMarket(ORDER_TYPE_BUY, user03, user02, user01, user04);
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))(*manager).ToMarket(ORDER_TYPE_SELL, user03, user02, user01, user04);
        }
        if (def_SHIFT_Press(BtnStatus) != def_CTRL_Press(BtnStatus))
        {
// This point ...
                if (def_BtnLeftClick(BtnStatus) && (mem == 0)) (*manager).CreateOrder(def_SHIFT_Press(BtnStatus) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, mem = Price, user03, user02, user01, user04);
        }else mem = 0;
}

黄色で示した部分が、テイクプロフィットラインとストップロスラインを表示するための呼び出しを追加する箇所です。しかし、1つだけ重要なことがあります。それは、これらの行のコードをどこに書けばいいのかということです。

これに対する最良の代替案は(誰もが納得すると思いますが)、C_Mouseクラスにこのコードを追加することです。マウスを外すと、線も消えるようにです。次をおこないます。C_Mouseクラスを開いて、テイクプロフィットラインとストップロスを表す線を作成しましょう。

ただし、今までとはちょっと違うことをします。OnChartEventではなく、C_Mouseクラス内のイベントハンドラに行を追加することにします。EAのコードに他の変更を加えなければならないという事実にもかかわらず、これはより良い方法ですが、これは後回しにします。C_Mouse.mqhのヘッダーファイルを開き、必要なものをすべて実装してみましょう。

まず最初に、以下のように新しい定義をいくつか追加します。

#define def_PrefixNameObject    "MOUSE_"
#define def_MouseLineName       def_PrefixNameObject + "H"
#define def_MouseLineTake       def_PrefixNameObject + "T"
#define def_MouseLineStop       def_PrefixNameObject + "S"
#define def_MouseName           "MOUSE_H"

なお、古い定義は削除されています。プログラミングが気持ちいいのは変わりませんが、ちょっと違う方法を使用します。プログラミングの手間を減らすために、作成手順を別のやり方で修正しましょう。

                void CreateLineH(void)
                void CreateLineH(const string szName, const color cor)
                        {
                                ObjectCreate(m_Infos.Id, def_MouseName, OBJ_HLINE, 0, 0, 0);
                                ObjectSetString(m_Infos.Id, def_MouseName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(m_Infos.Id, def_MouseName, OBJPROP_BACK, false);
                                ObjectSetInteger(m_Infos.Id, def_MouseName, OBJPROP_COLOR, m_Infos.Cor);
                                ObjectCreate(m_Infos.Id, szName, OBJ_HLINE, 0, 0, 0);
                                ObjectSetString(m_Infos.Id, szName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(m_Infos.Id, szName, OBJPROP_BACK, false);
                                ObjectSetInteger(m_Infos.Id, szName, OBJPROP_COLOR, cor);
                        }

これで、すべての線がユニークな方法で作成され、名前と色を指定するだけでよくなりました。色を格納するために、さらに2つの変数を作成しなければなりませんでしたが、これらはここで示す必要はないと思います。では、コンストラクタに移りましょう。今度は、以前よりも多くのデータを受け取る必要があるので、以下でそれを見てみましょう。

                C_Mouse(const color corPrice, const color corTake, const color corStop, const double FinanceStop, const double FinanceTake, const uint Leverage)
                        {
                                m_Infos.Id        = ChartID();
                                m_Infos.CorPrice  = corPrice;
                                m_Infos.CorTake   = corTake;
                                m_Infos.CorStop   = corStop;
                                m_Infos.PointsTake= FinanceToPoints(FinanceTake, Leverage);
                                m_Infos.PointsStop= FinanceToPoints(FinanceStop, Leverage);
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_MOUSE_MOVE, true);
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, true);
                                CreateLineH(def_MouseLineName, m_Infos.CorPrice);
                        }

すでに述べたように、いくつかの変数を作らなければなりませんでしたが、機能面で得られるものに比べれば、その値段は小さいです。以下のことに注意してください。 金融値をポイントに変換するのにEAの呼び出しを待つことはしません。今、ここでおこないます。 変数へのアクセスは関数を呼び出すよりずっと速いので、これは後で時間を節約することになります。しかし、デストラクタはどうでしょう。実際には、より難しいということはありません。そこで必要なのは、オブジェクトの削除を担当する関数の型を変更することです。以下でご覧ください。

                ~C_Mouse()
                        {
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, false);
                                ObjectsDeleteAll(m_Infos.Id, def_PrefixNameObject);
                                ObjectDelete(m_Infos.Id, def_MouseName);
                        }

この関数は、ある特定の方法で始まる名前を持つすべてのオブジェクトを削除することができます。 様々なシーンで活躍し、汎用性が高いです。ここで、最後のプロシージャを変更する必要があります。値幅制限のラインをどのように実装したのか、以下のコードで見てみましょう。

                void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
                        {
                                int w;
                                datetime dt;
                                static bool bView = false;
                                
                                switch (id)
                                        {
                                                case CHARTEVENT_OBJECT_DELETE:
                                                        if (sparam == def_MouseName) CreateLineH();
                                                        if (sparam == def_MouseLineName) CreateLineH(def_MouseLineName, m_Infos.CorPrice);
                                                        break;
                                                case CHARTEVENT_MOUSE_MOVE:
                                                        ChartXYToTimePrice(m_Infos.Id, (int)lparam, (int)dparam, w, dt, m_Infos.Price);
                                                        ObjectMove(m_Infos.Id, def_MouseName, 0, 0, m_Infos.Price = AdjustPrice(m_Infos.Price));
                                                        ObjectMove(m_Infos.Id, def_MouseLineName, 0, 0, m_Infos.Price = AdjustPrice(m_Infos.Price));
                                                        m_Infos.BtnStatus = (uint)sparam;
                                                        if (def_CTRL_Press(m_Infos.BtnStatus) != def_SHIFT_Press(m_Infos.BtnStatus))
                                                        {
								if (!bView)
								{
									if (m_Infos.PointsTake > 0) CreateLineH(def_MouseLineTake, m_Infos.CorTake);
									if (m_Infos.PointsStop > 0) CreateLineH(def_MouseLineStop, m_Infos.CorStop);
									bView = true;
								}
								if (m_Infos.PointsTake > 0) ObjectMove(m_Infos.Id, def_MouseLineTake, 0, 0, m_Infos.Price + (m_Infos.PointsTake * (def_SHIFT_Press(m_Infos.BtnStatus) ? 1 : -1)));
								if (m_Infos.PointsStop > 0) ObjectMove(m_Infos.Id, def_MouseLineStop, 0, 0, m_Infos.Price + (m_Infos.PointsStop * (def_SHIFT_Press(m_Infos.BtnStatus) ? -1 : 1)));
                                                        }else if (bView)
                                                        {
                                                                ObjectsDeleteAll(m_Infos.Id, def_PrefixNameObject);
                                                                bView = false;
                                                        }
                                                        ChartRedraw();
                                                        break;
                                        }
                        }

まず、2行の古いコードを削除し、次に2行の新しいコードを追加する必要がありました。しかし、重要なのは、マウスの移動イベントを処理するときです。そこで、新しい行を何行か追加します。まず、SHIFTキーやCTRLキーが押されているが、同時に押されていないかどうかを確認し、押されている場合は次のステップに進みます。

ここで、結果がFalseの場合、チャート上にリミットラインが表示されているかどうかを確認します表示されている場合は、マウスラインをすべて削除します。MetaTrader 5では、オブジェクトが画面から取り除かれたことを通知するイベントが即座に生成されるため、これは問題ではありません。スクリーンのイベントハンドラが呼ばれたら、価格線をチャートに戻すように案内されます

しかし、SHIFTキーやCTRLキーを押しながらだと、リミットラインが表示される瞬間に話を戻しましょう。この場合、画面上にすでに線があるかどうかを確認します.そして、そうでない場合は、チャート上に奇妙な要素は必要ないので、値が0より大きい間に作成します。呼び出しのたびにこれらのオブジェクトを再作成する必要がないように、これを完了とマークします。そして、プライスラインの位置によって、それぞれの場所に配置します


結論

手動で操作するために、EAシステムを作成しました。次回の記事で考える、次の大きなステップへの準備が整いました。システムが自動的に何かをおこなうことができるように、トリガーを追加します。それが終わったら、このような手動取引EAを完全自動EAにするために何が必要なのかを紹介します。次回は、EAの作業を自動化し、人間の判断を取引から排除する方法について説明します。


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

添付されたファイル |
エキスパートアドバイザー(EA)の選び方:取引ボットを却下するための20の強力な基準 エキスパートアドバイザー(EA)の選び方:取引ボットを却下するための20の強力な基準
この記事では、「どうやって正しいエキスパートアドバイザーを選べばいいのか」という問いに答えようと思います。ポートフォリオに最適なのはどれでしょうか。また、市場で入手できる大規模な取引ボットリストをどのようにフィルタリングすればいいのでしょうか。この記事では、エキスパートアドバイザーを却下するための20の明確で強力な基準を紹介します。それぞれの基準が提示され、よく説明されているので、より持続的な判断ができ、より収益性の高いエキスパートアドバイザーを集めることができるようになります。
ニューラルネットワークの実験(第3回):実用化 ニューラルネットワークの実験(第3回):実用化
この連載では、実験と非標準的なアプローチを使用して、収益性の高い取引システムを開発し、ニューラルネットワークがトレーダーに役立つかどうかを確認します。ニューラルネットワークを取引に活用するための自給自足ツールとしてMetaTrader 5にアプローチします。
Murrayシステム再訪問 Murrayシステム再訪問
グラフィカルな価格分析システムは、当然ながらトレーダーの間で人気があります。今回は、有名なレベルを含む完全なMurray(マレー)システム、および現在の価格ポジションを評価し、取引を決定するための有用な他のテクニックについて説明します。
自動で動くEAを作る(第04回):手動トリガー(I) 自動で動くEAを作る(第04回):手動トリガー(I)
今日は、自動モードでシンプルかつ安全に動作するエキスパートアドバイザー(EA)を作成する方法を紹介します。