![一からの取引エキスパートアドバイザーの開発(第7部):価格別出来高の追加(I)](https://c.mql5.com/2/49/Developing_a_trading_Expert_Advisor_from_scratch_012_600x314.jpg)
一からの取引エキスパートアドバイザーの開発(第7部):価格別出来高の追加(I)
はじめに
ある程度の自信を持って取引するには、チャートにはこのインジケータが必須です。このインジケータはよく、テープリーディングを好むトレーダーによって使用されますが、プライスアクションの分析のみに基づいて取引するのにも活用できます。これは特定の価格帯で発生した取引量を分析するために活用できる、非常に便利な水平方向のボリューム指標です。ただし、このインジケータを正しく読み取るのは難しいものです。記事の最後にリンクを貼っておきますので、詳しくご覧ください。
インジケータの読み方をどう解釈するかはこの記事の範囲外なので、ここでは割愛します。この記事の目的は、MetaTrader 5プラットフォームのパフォーマンスを低下させずに、このインジケータを設計し作成する方法を示すことです。ここで興味深い事実があります。このインジケータはリアルタイムで更新されるべきだと考える方が多いのですが、実は本当にわずかであれば、多少の遅れは許容範囲だということです。私自身の経験では、1秒程度の情報更新の遅れは大問題にはなっていません。ただ、真のリアルタイム性を重視するのであれば、ちょっとした工夫が必要でしょう。インジケータ自体ではなく、エキスパートアドバイザー(EA)がこのインジケータを呼び出す箇所を変更し、リアルタイムで呼び出しが発生するようにする必要があります。ただし、パフォーマンスへの影響はほとんどないと思いますので、遅延は無視しても大丈夫です。
インターフェイス
価格別出来高クラスのコントロールインターフェイスは非常にシンプルですが、完全にコントロールするためには、インジケータが適用されるチャートのプロパティを正しく確保する必要があります。プロパティは下図のようになります。主要なコントロールは強調表示されています。
グリッドが表示されていない場合、以下のアニメーションのようにインジケータのサイズを変更することはできません。インターフェイスは非常にシンプルで直感的です。コントロールは2つだけで、1つはサイズを示すもの、もう1つはボリューム分析の開始点を示すものです。
一般的に、このインジケータはかなり有効であり、実装や構築において非常に興味深いものです。今回は、その最も基本的なレベルで作業をおこない、次回以降に改良を加えていきます。
インターフェイスについてはもう言うことがないので、コードの実装に移りましょう。
実装
インジケータ作成時の作業をできるだけ少なくするために、ソースコードを分割し、また、いくつかの修正と追加をおこないます。必要なコードの多くはすでに別の場所に書かれているので、まずはコードを部分に分解してみます。その主な部分はC_Wallpaperクラスにあります。何をするのでしょうか。ビットマップに基づいたインジケータを作成するのでしょうか。確かに、コンピュータ画面上の画像はすべてBITMAPとして扱われるべきなのですが、それは特別な方法で構築されるべきです。新しいC_Wallpaperオブジェクトクラスは、次のようになります。
class C_WallPaper : public C_Canvas { protected: enum eTypeImage {IMAGEM, LOGO, COR}; //+------------------------------------------------------------------+ private : public : //+------------------------------------------------------------------+ ~C_WallPaper() { Destroy(); } //+------------------------------------------------------------------+ bool Init(const string szName, const eTypeImage etype, const char cView = 100) { if (etype == C_WallPaper::COR) return true; if (!Create(szName, 0, 0, Terminal.GetWidth(), Terminal.GetHeight())) return false; if(!LoadBitmap(etype == C_WallPaper::IMAGEM ? "WallPapers\\" + szName : "WallPapers\\Logos\\" + _Symbol, cView)) return false; ObjectSetInteger(Terminal.Get_ID(), szName, OBJPROP_BACK, true); return true; } //+------------------------------------------------------------------+ void Resize(void) { ResizeBitMap(Terminal.GetWidth(), Terminal.GetHeight()); } //+------------------------------------------------------------------+ };
C_WallpaperクラスとC_VolumeAtPriceクラスで共通する部分を削除し、すべてを別のクラス(C_C_Canvasクラス)にまとめたため、コードははるかにコンパクトになっています。
でも、MetaTrader 5のC_Canvasクラスを使用しないのはなぜでしょうか。これは、実用的というよりも個人的なものです。私は、自分が書いたり開発したりするすべてのものをコントロールするのが好きなのです。これは本当に必要だというよりは、Cプログラマーの悪い癖だと思います。これが、画面にオブジェクトを描画するためのクラスを作成した理由です。もちろん、MetaTrader 5にすでにあるクラスを利用することも可能です。次に、今回のメインであるC_VolumeAtPriceクラスに注目してみましょう。このクラスには、下の表に示す7つの機能があります。
関数 | 詳細 | アクセスタイプ |
---|---|---|
Init | ユーザーが指定した値でクラスを初期化する | 一般 |
Update | 指定された間隔の価格別出来高データを更新する | 一般 |
Resize | チャート上の価格別出来高の画像の大きさを変更し、一部の詳細な分析を容易にする | 一般 |
DispatchMessage | オブジェクトクラスへのメッセージ送信に使用する | 一般 |
FromNowOn | システム変数を初期化する | private |
SetMatrix | ボリュームデータの行列を作成し管理する | private |
Redraw | ボリュームの画像を作成する | private |
次に、システムの実装に移りましょう。まず、以下のコードで変数を宣言します。
#define def_SizeMaxBuff 4096 //+------------------------------------------------------------------+ #define def_MsgLineLimit "Starting point from Volume At Price" //+------------------------------------------------------------------+ class C_VolumeAtPrice : private C_Canvas { #ifdef macroSetInteger ERROR ... #endif #define macroSetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Infos.szObjEvent, A, B) private : uint m_WidthMax, m_WidthPos; bool m_bChartShift, m_bUsing; double m_dChartShift; struct st00 { ulong nVolBuy, nVolSell, nVolTotal; long nVolDif; }m_InfoAllVaP[def_SizeMaxBuff]; struct st01 { ulong memTimeTick; datetime StartTime, CurrentTime; int CountInfos; ulong MaxVolume; color ColorSell, ColorBuy, ColorBars; int Transparency; string szObjEvent; double FirstPrice; }m_Infos;
このコードで注目していただきたいのは、強調表示された部分です。この部分は、別のファイルからの定義がファイルで使用する定義と競合しないようにします。確かに、MQL5のコンパイラでは、既存の定義を上書きしようとすると警告が表示されますが、場合によっては解決する方法がわかりにくいこともあります。したがって、少し負担を減らすために、上記のコードで強調表示されたテストを使用します。コードの残りの部分には特に面白くありません。def_SizeMaxBuffの定義だけには注意する必要があります。これは、ボリュームデータ配列のサイズを示しており、必要であればこの値を別のものに変更することもできますが、テスト結果によると、ほとんどの場合、この値で十分なようです。安値と価格の間の変動をティック単位で表すため、現在の値で非常に広範囲のケースを処理できます。
Init機能:すべてが始まる場所
すべての変数を正しく初期化するのがこの関数です。EAでは、以下のように呼び出されます。
//.... Initial data.... input color user10 = clrForestGreen; //Take Profit line color input color user11 = clrFireBrick; //Stop line color input bool user12 = true; //Day Trade? input group "Volume At Price" input color user15 = clrBlack; //Color of bars input char user16 = 20; //Transparency (from 0 to 100 ) //+------------------------------------------------------------------+ C_SubWindow SubWin; C_WallPaper WallPaper; C_VolumeAtPrice VolumeAtPrice; //+------------------------------------------------------------------+ int OnInit() { Terminal.Init(); WallPaper.Init(user03, user05, user04); if ((user01 == "") && (user02 == "")) SubWin.Close(); else if (SubWin.Init()) { SubWin.ClearTemplateChart(); SubWin.AddThese(C_TemplateChart::SYMBOL, user02); SubWin.AddThese(C_TemplateChart::INDICATOR, user01); } SubWin.InitilizeChartTrade(user06, user07, user08, user09, user10, user11, user12); VolumeAtPrice.Init(user10, user11, user15, user16); // ... Rest of the code
ここにあるパラメータは余り多くはなく、主にインジケータが使用する色に関する情報を表します。次に、この関数の内部コードを見てみましょう。以下のコードは、初期化の様子を示しています。
void Init(color CorBuy, color CorSell, color CorBar, char cView) { m_Infos.FirstPrice = Terminal.GetRatesLastDay().open; FromNowOn(macroSetHours(macroGetHour(Terminal.GetRatesLastDay().time), TimeLocal())); m_Infos.Transparency = (int)(255 * macroTransparency(cView)); m_Infos.ColorBars = CorBar; m_Infos.ColorBuy = CorBuy; m_Infos.ColorSell = CorSell; if (m_bUsing) return; m_Infos.szObjEvent = "Event" + (string)ObjectsTotal(Terminal.Get_ID(), -1, OBJ_EVENT); CreateObjEvent(); m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT); m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE); ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true); ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1); Create("VaP" + (string)MathRand(), 0, 0, 1, 1); Resize(); m_bUsing = true; };
ご覧のとおり、すべてがとてもシンプルですが、ここにはコードを面白くする特徴があります。そのうち1つはTerminal.GetRatesLastDay().openです。これは奇妙に見えるかもしれませんが、オブジェクト指向プログラミング(OOP)の原則に従えば、実はごく普通です。OOPの原則の1つには、「クラスの内部変数には、クラスの外部からは一切アクセスできないようにする」というものがありますが、それではどうやってクラス内の変数の値を取得するのでしょうか。正しい方法は、OOPにしか登場しない形式を使うことです。GetRatesLastDay関数がC_Terminalクラスの中でどのように宣言されているかを見てみましょう。これは、以下のコードで見ることができます。
inline MqlRates GetRatesLastDay(void) const { return m_Infos.Rates; }
実際にどのような動作するのか見てみましょう。まずは予約語のinlineです。これは、コードをすべて現れる位置に配置するようコンパイラに指示するものです。コンパイラは、関数呼び出しを生成する代わりに、関数のコードをすべて、関数が参照される箇所に実際にコピーします。これにより、メモリ消費量が少なくなり、コードの実行が速くなります。しかし、特定のケースで実際に起こるのは、m_Infos.Rates変数が参照されるということです。この変数は型なので、MqlRates構造体の値にアクセスすることができます。この場合、変数参照のアドレスは渡しません。ただし、コードを高速化するために、参照のアドレスを渡す場合もあり、その場合、禁止されているはずであるにかかわらず、クラス内で変数の値を変更することが可能になってしまいます。これを防ぐために、予約語のconst を使って、その変数がクラス以外では決して変更されないことを保証します。C++の多くの予約語はMQL5にも文書化された形で存在します。中にはまだ文書化されていないものもありますが、C++に非常に近いためMQL5の一部になっています。記事の最後には、C++についてもっと学んで同じ知識をMQL5プログラミングに活かしたいという方のために、リンクを追加しています。
さて、Init関数のコードの中には興味深い部分があります。この部分が何をしているのかを説明するために、以下で強調表示してあります。
m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT); m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE); ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true); ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1);
EAを起動するとチャートが変更されますが、ユーザーがシャットダウンすると初期状態にリセットされるのも良い慣行です。そこで、チャートのスクロール設定を保存してから、最小限のスクロールを作成します。これは強調表示されたポイントでおこなわれるので、寸法を調整できるようにするためには、チャート上のグリッドを見る必要があります。これは、冒頭に示したように、インタラクティブにおこなわれます。詳しくはCHART_SHIFTを参照してください。
画面上のオブジェクトを固定する
このクラスの内部関数は非常にシンプルですが、特に注意すべき点があります。1つ目は、ボリューム解析の開始を示すドットをユーザーが外せないようにするセキュリティシステムです。
ドットは非常に小さいので、本当に気づくには注意が必要です。
注意事項:分析ポイントを変更したい場合は、チャートの時間枠に注意してください。例えば、9:00から9:02に分析を移動させる必要がある場合、1分または2分の時間枠を使用する必要があります。そして、例えば5分足のチャートを使うと、これができなくなります。
次に、ユーザーが誤ってこの要素を削除しないようにする必要があります。これは、以下のコードでおこなわれます。
void DispatchMessage(int iMsg, string sparam) { switch (iMsg) { // ... The inside of the code case CHARTEVENT_OBJECT_DELETE: if ((sparam == m_Infos.szObjEvent) && (m_bUsing)) { m_bUsing = false; CreateObjEvent(); Resize(); m_bUsing = true; } break; } };
オブジェクトが削除されたことががクラスで認識されると、直ちにそのオブジェクトが再作成されるため、クラスに必要なオブジェクトがないまま、EAが再起動されることを防ぐことができます。ユーザーが機密性の高いオブジェクトを削除しないようにする必要がある場合は、コードに示したモデルを使用します。ただし、イベントがEAに通知されるようにするために、次のコードを追加する必要があります。
ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);
この単純な行で、MetaTrader 5がオブジェクトの削除を報告するようになります。詳細はCHART_EVENT_OBJECT_DELETEを参照してください。
価格別出来高チャートの作成
これはクラスの中心で、3つの関数(1つはpublic、2つはprivate)があります。まず、public関数は以下のようになります。
inline virtual void Update(void) { MqlTick Tick[]; int i1, p1; if (m_bUsing == false) return; if ((i1 = CopyTicksRange(Terminal.GetSymbol(), Tick, COPY_TICKS_TRADE, m_Infos.memTimeTick)) > 0) { if (m_Infos.CountInfos == 0) { macroSetInteger(OBJPROP_TIME, m_Infos.StartTime = macroRemoveSec(Tick[0].time)); m_Infos.FirstPrice = Tick[0].last; } for (p1 = 0; (p1 < i1) && (Tick[p1].time_msc == m_Infos.memTimeTick); p1++); for (int c0 = p1; c0 < i1; c0++) SetMatrix(Tick[c0]); if (p1 == i1) return; m_Infos.memTimeTick = Tick[i1 - 1].time_msc; m_Infos.CurrentTime = macroRemoveSec(Tick[i1 - 1].time); Redraw(); }; };
強調表示された行は、システムにとって非常に重要なものです。起動された時、システムはどこから手をつけていいのかわかりません。これらの行は、これらのポイントを更新し、分析がどこで始まり、開始価格が何であったかをユーザーに知らせ、システムが内部テーブルを作成できるようにするものです。システムは常に新しいティックが到着するのを待ち、到着した場合、データを解析し、収集して画面に表示する必要があります。関数は次の通りです。
inline void SetMatrix(MqlTick &tick) { int pos; if ((tick.last == 0) || ((tick.flags & (TICK_FLAG_BUY | TICK_FLAG_SELL)) == (TICK_FLAG_BUY | TICK_FLAG_SELL))) return; pos = (int) ((tick.last - m_Infos.FirstPrice) / Terminal.GetPointPerTick()) * 2; pos = (pos >= 0 ? pos : (pos * -1) - 1); if ((tick.flags & TICK_FLAG_BUY) == TICK_FLAG_BUY) m_InfoAllVaP[pos].nVolBuy += tick.volume; else if ((tick.flags & TICK_FLAG_SELL) == TICK_FLAG_SELL) m_InfoAllVaP[pos].nVolSell += tick.volume; m_InfoAllVaP[pos].nVolDif = (long)(m_InfoAllVaP[pos].nVolBuy - m_InfoAllVaP[pos].nVolSell); m_InfoAllVaP[pos].nVolTotal = m_InfoAllVaP[pos].nVolBuy + m_InfoAllVaP[pos].nVolSell; m_Infos.MaxVolume = (m_Infos.MaxVolume > m_InfoAllVaP[pos].nVolTotal ? m_Infos.MaxVolume : m_InfoAllVaP[pos].nVolTotal); m_Infos.CountInfos = (m_Infos.CountInfos == 0 ? 1 : (m_Infos.CountInfos > pos ? m_Infos.CountInfos : pos)); }
この関数は、価格での出来高値を保存/保持するだけなのであまり重要ではないのかもしれませんが、その中で強調表示されている行がシステムの中心となっています。この2行で何が起こっているのかを本当に理解するために、少し考えてみましょう。次のように考えてみてください。それぞれの価格を記憶し、それぞれのボリュームをメモするのと、価格が何であるかを気にしながらボリュームだけを記憶するのと、どちらが速いでしょうか。速いのは2番目の選択肢の方なので、ボリュームを保存して、価格がどこなのか調べてみましょう。でも、システムの最初の価格はどうするのでしょうか。初期値が必要だからです。初期値がなければ、すべてがバラバラになってしまいます。最初に取引されたティックの価格を使用するのはどうでしょうか。いいですね。完璧です。ただし、問題があります。価格が上がれば、それは素晴らしいことで、すべてのデータを配列に簡単に格納することができます。でも、もし落ちたらどうでしょうか。この場合、負の値を持つことになり、負のインデックスで配列にアクセスすることはできません。1つではなく2つの配列を使うこともできますが、そうすると無駄な負荷がかかってしまいます。簡単な解決策があります。下の表をご覧ください。
インデックスが正の場合は心配ありませんが、負の場合は双方向の配列を使っているため問題が発生します。ここで、ゼロの値は最初のティックの価格を表し、負の値は下降した価格、正の値は上昇した価格です。次に、双方向の場合にインデックスを2倍することで、真ん中の列を得ることができます。これは役には立たないようです。しかし、絶対値にして1を引くと、右の列になります。よく見ると、この右側の列で値が交互になっています。これは、上昇することはわかっているが、どれくらい上昇するのかわからない配列にアクセスするための完璧なインデックスです。そして、それはまさに2つの強調表示された行でおこなうことです。これらは、開始価格よりも高い値と低い値を交互に繰り返す配列インデックスを作成します。これは非常に良い解決策ですが、データを画面に表示できなければ意味がありません。これは、次の関数がおこなうことです。
void Redraw(void) { uint x, y, y1, p; double reason = (double) (m_Infos.MaxVolume > m_WidthMax ? (m_WidthMax / (m_Infos.MaxVolume * 1.0)) : 1.0); double desl = Terminal.GetPointPerTick() / 2.0; Erase(); p = m_WidthMax - 8; for (int c0 = 0; c0 <= m_Infos.CountInfos; c0++) { if (m_InfoAllVaP[c0].nVolTotal == 0) continue; ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, m_Infos.FirstPrice + (Terminal.GetPointPerTick() * (((c0 & 1) == 1 ? -(c0 + 1) : c0) / 2)) + desl, x, y); y1 = y + Terminal.GetHeightBar(); FillRectangle(p + 2, y, p + 8, y1, macroColorRGBA(m_InfoAllVaP[c0].nVolDif > 0 ? m_Infos.ColorBuy : m_Infos.ColorSell, m_Infos.Transparency)); FillRectangle((int)(p - (m_InfoAllVaP[c0].nVolTotal * reason)), y, p, y1, macroColorRGBA(m_Infos.ColorBars, m_Infos.Transparency)); } C_Canvas::Update(); };
この関数はボリュームチャートをプロットします。強調表示された部分はボリュームキャプチャ中に行われた計算を反転します。正しい位置に表示させるために、価格を少しずらしてバーの位置が正しくなるようにします。残りの関数は、単なる描画ルーチンです。ここで少し説明が必要です。FillRectangleの呼び出しが2つあることに注意を向けてください。なぜでしょうか。1回目の呼び出しは売り手と買い手のどちらの数量が多かったかを示し、2回目の呼び出しは実際に数量をプロットします。しかし、ボリューム帯を買い手と売り手に分けることで、一緒に作らないのはなぜなのでしょうか。理由は、ある価格帯で出来高が増えると、他のより小さな価格帯の分析に支障をきたし始めるということです。売りと買いのどちらのボリュームが大きかったのか、判断が難しくなります。このように配置すると、この問題がなくなり、データの読み取りが容易になって理解しやすくなります。その結果、チャートは下図のようになります。
他のすべてのクラス関数は、先に説明した関数の補助として機能するため、詳細を説明するほど重要ではありません。
結論
ここでは、非常にシンプルな「価格別出来高」を紹介しましたが、これは非常に有効なツールです。コーディングを学び始めていて、オブジェクト指向プログラミング(OOP)に焦点を当てたい方は、このコードを注意深く研究する必要があります。なぜなら、このコードにはいくつかの優れた概念があり、すべてのコードは100%オブジェクト指向のアプローチに基づいているからです。
アプリケーションには、現在の開発段階までのEAが含まれています。
お役立ちリンク
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/10302
![DoEasy - コントロール(第1部):最初のステップ](https://c.mql5.com/2/46/MQL5-avatar-doeasy-library-2.png)
![DoEasyライブラリのグラフィックス(第100部):拡張された標準グラフィックオブジェクトの処理を改善する](https://c.mql5.com/2/45/MQL5-avatar-doeasy-library3-2__4.png)
![DoEasy - コントロール(第2部):CPanelクラスでの作業](https://c.mql5.com/2/46/MQL5-avatar-doeasy-library-2__1.png)
![データサイエンスと機械学習(第01回):線形回帰](https://c.mql5.com/2/48/linear_regression__1.png)
![MQL5 - MetaTrader 5クライアントターミナルにビルトインされたトレードストラテジーの言語](https://c.mql5.com/i/registerlandings/logo-2.png)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索