
MQL5におけるイベント操作: オンザフライのMA期間変更
はじめに
短い本稿はMetaTrader 5プラットフォームの MQL5の新機能の一つに特化しています。それはMetaQuotes Software Corp.により開発されたものです。おそらく、本稿はやや出るのがおそかったかもしれません。(2009年9月~10月に発行されるべきでした。それならちょうどよかったのですが)しかし、この内容について似た記事は他にありませんでした。それ以上に、その頃はインディケータ内でイベントを扱う可能性など考えられませんでした。
チャートに適用されるシンプルな価格インディケータ(この場合は移動平均、ここでいうMAです。)がり、その平滑化期間を変更したいという状況を想像します。 MT4プラットフォームでは以下のようなオプションがありました。
- MetaEditorでは、エキスパートの入力パラメータを編集することができます。 (extern)それは、MA期間に影響を与え、ソースファイルをコンパイルします。
- MetaEditorに切り替えることなく、ターミナルウィンドウ内でインディケータ プロパティのダイアログボックスを開くことができ、そこで対応する入力パラメータの編集が可能です。
- Win32 APIライブラリを開き、メッセージ獲得関数を見つけることができます。それからキーボードからのイベントに応答するようにインディケータコードを微調整します。
周知のように、最小の労力で作業する欲求が最大の進歩への原動力なのです。さて、MT5 の新しいプラットフォームのおかげで、ユーザーがインディケータ イベントを扱うことができるようになりました。これで上記の可能性をとばしてキーを一つ押すだけでインディケータ パラメータを変更することができます。本稿はこの問題解決の技術的実装について述べていきます。
タスク割り当てと問題
われわれの試みで使用するソースコードはクライアント端末に搭載して出荷されているものです。変更を加えていないソースコードファイル (Custom Moving Average.mq5) は本稿の最後に添付されています。
ここではソースコードを分析することはせず、特にMQL4の原版からの変更箇所について比較することはしません。 もちろん、ある箇所ではかなり変更が加えられていて、それが必ずしも目に見えるものでもありません。計算の基本構築に関連して対応する説明は、オンラインヘルプのフォーラムに見ることができます。
それ以外、MQL4のインディケータの主だった箇所は変更されることはありません。コードに加えられた変更の80%は少なくとも問題解決を意図しているもので、『ブラックボックス』としてのインディケータの計算関数という考えに基づき変更されています。
目標とすることの一例は以下のようなものです。このインディケータをチャートに適用し、ある時点でそれがゼロシフトと期間10を伴う指数MA (EMA) を表示したとします。われわれの目標は、シンプルMA (SMA)の平滑化期間を3ずつ(13まで)増やしていくことです。 そのために以下の流れが予測されます。
- MA表示が指数からシンプル(MAタイプの変更)に変わるまで数回 タブキーを押します。
- シンプルMAの期間を3ずつ増やすため、キーボード本体の上矢印キーを3回押します。
- MAを5バーずつ右へオフセットするため、テンキーの上矢印キー(8)を押します。
最もわかりやすい解決法は、インディケータコードにOnChartEvent()関数を挿入し、キーストロークイベント ハンドラを書くことです 変更リストによると、MetaTrader 4クライアント端末では245と246をビルドしhttps://www.mql5.com/en/forum/53/page1/#comment_2655、
MetaTrader 5クライアント端末が245と246をビルドするということです。
…
MQL5:カスタム インディケータによるイベントハンドリングの機能がつけ足されました。Expert Advisorsによるものと類似しています。
これで問題なくインディケータに新しいイベントハンドラを追加することができます。しかし、このためにはコードにまだ少しばかり変更を加える必要があります。
まずMQL5におけるインディケータの外部パラメータ状態が変更されました。これは、コードでは変更することができません。唯一の方法は、クライアント端末のプロパティ ダイアログボックス経由で変更することです。一般的に、急い変更を加える必要があるとき、この制約は簡単に跳び越えて進みます。ただ外部パラメータ値をインディケータの新規グローバル変数にコピーするだけです。すると、これら新規変数があたかもインディケータの外部パラメータであるかのようにすべての計算が行われます。一方でこの場合、値がユーザーに誤解を与える外部パラメータの実行可能性はなくなります。もうこれらパラメータは不要となるのです。
これで、インディケータには、外部(入力)パラメータは存在しません。外部パラメータの役目をする変数はターミナルグローバル変数または略してTGVということになります。かつてインディケータの外部パラメータに影響を与えたTGVを見たければ、ただ端末のF3キーを押せばよいだけです。これ以上インディケータ パラメータを簡単に操作する方法はありません。
次に(そしてこれが重要です)インディケータの外部パラメータに対するいかなる変更についても一からから今一度履歴の全値に関して計算をし直すのです。言い換えると、通常はインディケータの初期の初期にのみ行われる計算を行うのです。インディケータの計算最適化は保持され、今度はより緻密になります。
変更を加えたインディケータの第一バージョンのコード部分をいくつか以下に挙げています。コード全体は本稿の末尾に添付しています。
『標準』バージョン:標準的なインディケータ ソースコードにおける変更記述
外部パラメータはもはや外部ではなく、ただグローバル変数となっています。
インディケータの外部パラメータはすべて入力モディファイアを失いました。通常、それらをグローバルにすることさえできませんが、慣習どおりにそれをすることにしました。
int MA_Period = 13; int MA_Shift = 0; ENUM_MA_METHOD MA_Method = 0; int Updated = 0; /// Indicates whether the indicator has updated after changing it's values
最初の3つの選択肢は、期間、オフセット、MAタイプで4つ目は更新です。これは、MAパラメータを変更するとき、計算最適化に影響を与えます。説明は以下の数行です。
バーチャルキーコード
バーチャルキー用にコードを入力します。
#define KEY_UP 38 #define KEY_DOWN 40 #define KEY_NUMLOCK_DOWN 98 #define KEY_NUMLOCK_UP 104 #define KEY_TAB 9
これらは、『上向き矢印』と『下向き矢印』キーのコードで、テンキー(「8」と「2」)やタブキーと似ています。実は同じコードが<MT5dir>\MQL5\Include\VirtualKeys.mqhファイルにあります(VK_XXX定数という別の名前で)が、ここではそれはそのままにします。
線形加重移動平均(LWMA) を計算して行う関数内の小さなコード修正
CalculateLWMA()関数にちょっとした調整を施しました。原版ではweightsum変数は静的モディファイアを用いて宣言されていました。見てわかるように、開発者がそうしたのは、この関数の最初の呼び出し時に前もって計算する必要があるからです。コード内ではこの関数にそれ以上の変更はありません。以下はこの関数の原形コードです。そこには計算に関連する部分とweightsum使用にはコメントがつけられています。
void CalculateLWMA(int rates_total,int prev_calculated,int begin,const double &price[]) { int i,limit; static int weightsum; // <-- using weightsum double sum; //--- first calculation or number of bars was changed if(prev_calculated==0) // <-- using weightsum { weightsum=0; // <-- using weightsum limit=InpMAPeriod+begin; // <-- using weightsum //--- set empty value for first limit bars for(i=0;i<limit;i++) ExtLineBuffer[i]=0.0; //--- calculate first visible value double firstValue=0; for(i=begin;i<limit;i++) // <-- using weightsum { int k=i-begin+1; // <-- using weightsum weightsum+=k; // <-- using weightsum firstValue+=k*price[i]; } firstValue/=(double)weightsum; ExtLineBuffer[limit-1]=firstValue; } else limit=prev_calculated-1; //--- main loop for(i=limit;i<rates_total;i++) { sum=0; for(int j=0;j<InpMAPeriod;j++) sum+=(InpMAPeriod-j)*price[i-j]; ExtLineBuffer[i]=sum/weightsum; // <-- using weightsum } //--- }
以前、このバリアントはとてもよく動作しました。が、『インディケータ+アドバイザ』をタンデムで実行(これについては本稿末尾で述べています)した際、まさにこのMAタイプに問題が発生しました。主な問題のひとつは、上記で記述された状況によって生じたものです。すなわちweightsumが静的変数であり、この変数はMAパラメータがオンザフライの変更以来常に増え続け、最初から再計算する必要があったのです。
weightsum値(1からMA期間までの整数の合計に等しい。この目的のために、等差数列の合計を出す簡単な式があります。)を直接的にすぐに計算し、同時に静的という状態を否定する一番簡単な方法で、それは私がやったことです。そして、静的モディファイアを使った以前のweightsum宣言の代わりにそれを使わずに宣言を行い、『正しい』値で初期化をします。そうして『変数集積』の初期ループを除去します。
int weightsum = MA_Period *( MA_Period + 1 ) / 2;
これですべて正しく動作します。
ハンドラとしてのOnCalculate()関数
OnCalculate()関数には多くの変更を加える必要がありました。そのコードをここにすべて書き出します。
int OnCalculate(const int rates_total, const int prev_calculated, /// Mathemat: full recalculation! const int begin, /// Mathemat: full recalculation! const double &price[]) { //--- check for bars count if(rates_total<MA_Period-1+begin) return(0);// not enough bars for calculation //--- first calculation or number of bars was changed if(prev_calculated==0) ArrayInitialize(LineBuffer,0); //--- sets first bar from what index will be draw PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,MA_Period-1+begin); //--- calculation (Mthmt - optimized by Mathemat) if( GlobalVariableGet( "Updated" ) == 1 ) { if(MA_Method==MODE_EMA) CalculateEMA( rates_total,prev_calculated,begin,price); if(MA_Method==MODE_LWMA) CalculateLWMA_Mthmt(rates_total,prev_calculated,begin,price); if(MA_Method==MODE_SMMA) CalculateSmoothedMA(rates_total,prev_calculated,begin,price); if(MA_Method==MODE_SMA) CalculateSimpleMA( rates_total,prev_calculated,begin,price); } else { OnInit( ); /// Mthmt if(MA_Method==MODE_EMA) CalculateEMA( rates_total,0,0,price); if(MA_Method==MODE_LWMA) CalculateLWMA_Mthmt(rates_total,0,0,price); if(MA_Method==MODE_SMMA) CalculateSmoothedMA(rates_total,0,0,price); if(MA_Method==MODE_SMA) CalculateSimpleMA( rates_total,0,0,price); GlobalVariableSet( "Updated", 1 ); Updated = 1; } //--- return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+
主な変更は『一からの』インディケータ計算を完了するために必要と判断されたものです。どういうことかというと、ユーザーがキーボード操作をしてMA期間を13から14に変えたら、その計算について以前に行った最適化は役に立たなくなり、再びMAを計算する必要が出てきます。これが起こるのは更新された変数が0値のときです。(TGVはホットキーを押した後変更しますが、インディケータを再ドローするティックはまだ生成されていません。)
ただ、付け加えるなら、以前なら明確にOnInit()関数を呼ぶ必要がありました。それはインディケータの省略名を変更する必要があったからで、それはカーソルが線上を動くと表示されました。 最初のMA計算後、更新されたTGVは1に設定されます。またオンザフライでインディケータのパラメータを変更するのに気が進まないのでない限り、それは最適化されたインディケータ計算への道を開くものです。
OnChartEvent()ハンドラ
下記はOnChartEvent()ハンドラのシンプルなコードです。
void OnChartEvent( const int id, const long &lparam, const double &dparam, const string &sparam ) { if( id == CHARTEVENT_KEYDOWN ) switch( lparam ) { case( KEY_TAB ): changeTerminalGlobalVar( "MA_Method", 1 ); GlobalVariableSet( "Updated", 0 ); Updated = 0; break; case( KEY_UP ): changeTerminalGlobalVar( "MA_Period", 1 ); GlobalVariableSet( "Updated", 0 ); Updated = 0; break; case( KEY_DOWN ): changeTerminalGlobalVar( "MA_Period", -1 ); GlobalVariableSet( "Updated", 0 ); Updated = 0; break; case( KEY_NUMLOCK_UP ): changeTerminalGlobalVar( "MA_Shift", 1 ); GlobalVariableSet( "Updated", 0 ); Updated = 0; break; case( KEY_NUMLOCK_DOWN ): changeTerminalGlobalVar( "MA_Shift", -1 ); GlobalVariableSet( "Updated", 0 ); Updated = 0; break; } return; }//+------------------------------------------------------------------+
ハンドラは次のように動作します。ホットキーを押します。するとchangeTerminalGlobalVar()補助関数が動作し始め、希望のTGVに正しく修正を加えます。このあと、更新されたフラグがゼロにリセットされティックを待ちます。ティックOnCalculate()を実装し、インディケータを最初から再ドローします。
TGVを正しく変更する補助関数
最後にOnChartEvent()で使用されるchangeTerminalGlobalVar()関数についてです。
void changeTerminalGlobalVar( string name, int dir = 0 ) { int var = GlobalVariableGet( name ); int newparam = var + dir; if( name == "MA_Period" ) { if( newparam > 0 ) /// Possible period is valid for MA { GlobalVariableSet( name, newparam ); MA_Period = newparam; /// Don't forget to change the global variable } else /// we do not change the period, because MA period is equal to 1 minimum { GlobalVariableSet( name, 1 ); MA_Period = 1; /// Don't forget to change the global variable } } if( name == "MA_Method" ) /// Here when you call the 'dir' it is always equal to 1, the dir value is not important { newparam = ( var + 1 ) % 4; GlobalVariableSet( name, newparam ); MA_Method = newparam; } if( name == "MA_Shift" ) { GlobalVariableSet( name, newparam ); MA_Shift = newparam; } ChartRedraw( ); return; }//+------------------------------------------------------------------+
本関数の主な使用目的は、新規『物理的限界』を考慮したMAパラメータの正確な計算 です。みてのとおり、MA期間をMAの遷移に規則性のない1より小さく設定することはできません。 しかし、MAタイプは ENUM_MA_METHOD列挙のメンバー数に応じて0~3となります。
確認です。動作しますが、『Cレベル』です。この対処法は?
では、チャートにわれわれのインディケータを適応し、散発的にMAパラメータを変えるホットキーを押しましょう。はい、これですべてが正しく動作します。が、ひとつ気持ちの悪い事実があります。というのも、TGVがすぐさま変更していることです。(F3キーを使ってTGVを呼び出すとこれを確認することができます。)しかしMAはすぐに再ドローしているわけではなく新規ティックが到着した時のみです。アクティブなティックフローでアメリカ式のセッションをしていたら、その遅延に気づくことはほとんどないでしょう。しかし、夜に起こったら、静かな時間帯、数分は再ドローを待つことができます。そして何が起こるのでしょう?
よく言われることですね。「ここにあるのがあんたが書いた結果だよ。」インディケータに245をビルドする以前、『エントリーポイント』はたった一つだけでした。OnCalculate()関数です。インディケータの最初の計算を行い、初期化し、完了するOnInit()関数や OnDeinit()関数のことではありません。いまでは、複数のエントリーポイントがあり、新規タイマーやChartEventイベントと連携しています。
かといって、新しいハンドラはそれが含むことをしているだけでOnCalculate()ハンドラと正式に連携しているわけではありません。そこで、『見知らぬ』OnChartEvent()ハンドラを『正しく』動作させる、すなわち即座にMAを再ドローするには何をすべきでしょうか?
通常、この条件を実装するには複数の方法があります。
- 『マトリョーシカ』(OnChartEvent()内でのOnCalculate()呼び出し):OnCalculate()関数呼び出しをこのハンドラに挿入し、そのパラメータをすべて前もって書きます。 OnChartEvent()ハンドラが最低一つの MAパラメータを変更することを表すので、全履歴に影響を与えることとなります。すなわち、また『最初から』計算しなおす必要がでてきます。が、計算の最適化はしません。
- コントロールをOnCalculate()関数の始まりへ移動させる『人工ティック』は、グラフィックバッファを変更します。明らかに『決まりきった』方法はありません。MT5に書かれているとおりです。(ぜんぶをぜんぶ当たったとは言えないでしょうが) 関心がおありなら、«API»、«PostMessageA»などを検索することも可能です。よって、ここではこのバリアントについての考察はいたしません。なぜなら記録にない機能はいつか変わらない保証はないからです。それが現実になるかもしれない、と思っています。
『マトリョーシカ』は役に立ちます!
われわれはすでに重要なポイントは押さえました。下記は関数の非常にシンプルなコードです。ただその呼び出しをOnChartEvent() ハンドラの
returnオペレータの直前に挿入するだけです。
int OnCalculate_Void() { const int rates_total = Bars( _Symbol, PERIOD_CURRENT ); CopyClose( _Symbol, PERIOD_CURRENT, 0, rates_total, _price ); OnCalculate( rates_total, 0, 0, _price ); return( 1 ); }//+------------------------------------------------------------------+
あとは、インディケータをコンパイルし、チャートに適用、そして通常はティック到着でコードは早く独立して動作するのがわかります。
この実装の不利な点は、クローズ価格がそのままprice[]配列にコピーされることです。お望みならば、CopyClose()関数を『設定』の『適用』フィールドにインディケータプロパティのダイアログボックスのタブを設定することで望むものと置き換えることは可能です。現在価格が基本的なもの(オープン、ハイ、ロー、クローズ)であれば、すでに対応するCopyXXXX()関数はすでに手にしています。もっと複雑な価格(中央値、Typical、加重) の場合、違う方法で配列を計算する必要があります。
配列履歴全体をコピーするCopyClose()関数が必要ないかどうかは定かではありません。一方、この関数は履歴があまり深くロードされていなければ十分早く動作します。1999年以前の履歴(約70万バー)でEURUSD H1に関するインディケータをチェックすると、インディケータは計算を行い速度低下はないことが判ります。そういった履歴では速度低下はCopyXXXX()関数によって生じるのではなく、履歴の最初から(これは必須です)より複雑化したインディケータの再計算が必要なため起こる可能性があるのです。
何点かの発見と結論
何がよりよいことでしょうか - 1件のインディケータ ファイル でしょうか、それとも『インディケータ+アドバイザ』のタンデムでしょうか?
これは簡単な質問ではありません。が、1件のインディケータファイルを持つことはよいことです。なぜなら、イベントハンドラを含むすべての関数が一か所に集まっているからです。
しかし、一般的な状況ではありませんが、もう一方でExpert Advisorのチャートに3~4のインディケータが適用されているとしましょう。また、それぞれのインディケータにはOnCalculate()に加えて独自のイベントハンドラが備わっていると仮定します。このモトリー・クルーで処理されているイベントに混乱をさけるため、すべてのイベントハンドラを許可されたインディケータ内の一か所、すなわち Expert Advisorに集めるのは合理的です。
ソフトウェア開発者は長年にわたり、われわれがインディケータ内でイベントを処理できるようにすることを考えてきました。2009年9月9日の非公式のベータ版リリースから(インディケータが『純粋な計算&数学的存在』とみなされていた頃で、計算速度などの機能により汚染されていない頃です)5か月が経過しました。同様に、『アイデアの純粋性』は我慢を強いられています。そして今プログラマの妄想に実在する混乱は解き放たれようとしています。しかし、バランスは常にどこか純粋だが制約を受けた考えとあまり清潔ではないが力強い能力の間にあるのです。
2009年9月~10月MT5ベータ版構築番号が200にも達しないとき、私は『Expert Adviser + インディケータ』タンデムを書き、コードをデバッグしました。それにより、オンザフライでMAパラメータの管理が可能となりましたが、『C級』でした。それはティックの到来によって更新されましたがすぐにではありませんでした。そのころ、このタンデムの考え方はソリューションの可能性にすぎませんでした。いまは打って変わってだれもがここに関心を抱いています。
そして、インディケータ機能性を『B級』にする方法が思いつきませんでした。それは前回バージョンでお見せしたようなところです。いま、関心をお持ちの方々により利便性のあるソリューションを提供できることをうれしく思います。
『マトリョーシカ』ビデオ
添付は私が作成した短編ビデオで、この稿で作成したものをお見せしています。MA曲線(ただ期間を変更しただけです。まず増加、それから減少へです)のなめらかな変遷はある意味目がくらむほどです。 これがマトリョーシカです。(有名なロシアの人形と類似しています)
もちろん、そのようなトリックは一からのインディケータの計算があまり時間を遣いすぎないときのみ有用です。このインディケータに含まれるシンプルなMAはこの要望に応えています。
信頼できない一面
以前のインディケータの外部パラメータは今端末のグローバル変数(TGV)です。それはF3キーを押すと見ることができます。グローバル変数ダイアログボックスを開いて、TGVの一つ(たとえば、MA期間)を変えたと仮定します。 みなさんはこの変更が即座にチャートのインディケータに反映されることを期待するでしょう。
ところが現時点では、端末にはユーザーによって作成されたTGV編集に対応するイベントがありません。(たとえば CHARTEVENT_GLOBALVAR_ENDEDIT)また、私が思うに、われわれはグローバル変数のダイアログボックス内でTGV変更を無効にすることはできません。 よって、ここでティック以外のいかなるイベントも計算できないのです。実際には何が起こるのでしょうか?
キーボードに触れなければ、次のティックで更新は『誤り』となります。Updated変数はゼロに設定されません。そして、『最適化された』インディケータ(変更されたTGVの以前の値に対応している)の計算が行われます。 この場合、正義を取り戻すために、われわれがアドバイスできるのは一つです。TGVを編集したあと、少なくともホットキーを一つは押します。それがTGVを変更し、Updated = 0に設定し、インディケータの再計算をすべて行います。
潜在的なユーザーや開発者はこの事実を記憶してください。
ソースコードとビデオの添付ファイル
最後にソースコードファイルを添付します。説明
- Custom Moving Average.mq5 - MT5出荷時に搭載されているMAのソースコード
- MyMA_ind_with_ChartEvent.mq5 - 初期(『C級』)の実装:ティック到来後にのみ行われるインディケータ更新
- MyMA_ind_with_ChartEvent_Matryoshka.mq5 - 第二 (おそらく『B級』)バリアント:インディケータはティックの到来を待たずに即座に更新
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/39





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索