
MVCデザインパターンとその可能なアプリケーション
はじめに
多くの開発者は、プロジェクトが成長してより複雑になり、新しい機能を取得して、コードがある種のスパゲッティに似たものになり始めるという段階を経験したと思います。プロジェクトはまだ終了していないのに、メソッドやメソッドが呼び出される場所、呼び出しが何故そこにあるのか、そして、すべてがどのように機能するかを思い出すことがすでに非常に困難になります。
時間の経過とともに、コードの作成者でさえコードを理解することがより難しくなります。別の開発者がこのコードを理解しようとすれば、なおさらです。その時点で、何らかの理由でコード作成者が不在の場合、タスクは実質的に解決できなくなります。コードが構造化されていなけれは、難易度が「Hello、world」を超えたコードの保守と変更が非常に困難になります。これが、デザインパターンが出現した理由の1つです。デザインパターンはプロジェクトに特定の構造をもたらし、より明確で視覚的に理解しやすくします。
MVCパターンとその目的
このパターンはかなり昔(1978年)に登場しましたが、最初の記述はずっと後の1988年に登場しました。それ以来、テンプレートはさらに発展し、新しいアプローチを生み出しています。
本稿では、複雑さや追加機能のない「クラシックMVC」について考察します。アイデアは、既存コードをモデル、ビュー、コントローラの3つの別々のコンポーネントに分割することです。MVCパターンによれば、これら3つのコンポーネントは独立して開発および保守できます。各コンポーネントは、新しいバージョンの作成とエラーの修正を行う開発者の個別のグループによって開発できます。明らかに、プロジェクト全体の管理がはるかに簡単になり、他の人がコードを理解する手助けともなります。
各コンポーネントを見てみましょう。
- ビュー:ビューは、情報の視覚的表現を担当し、一般的なケースでは、ユーザーにデータを送信します。同じデータをユーザーに提示するには、さまざまな方法があります。たとえば、データは、表、グラフ、チャートで同時に表すことができます。つまり、MVCベースのアプリケーションには複数のビューを含めることができます。ビューは、モデル内で何が起こっているかを知らずに、モデルからデータを受け取ります。
- モデル:モデルにはデータが含まれています。モデルは、データベースとの接続を管理し、要求を送信し、さまざまなリソースと通信します。また、データを変更して検証し、必要に応じて保存および削除します。モデルは、ビューがどのように機能し、いくつのビューが存在するかについては何も認識していませんが、ビューがデータを要求できるために必要なインターフェイスを備えています。ビューが実行できることは他にありません。つまり、ビューはモデルの状態を強制的に変更することはできず、この役割はコントローラによって実行されます。内部的には、モデルは、階層に配置された、または同等に機能する他のいくつかのモデルで構成できます。モデルは、前述の制限を除いて、この点で制限されていません。その内部構造はビューとコントローラから秘密に保たれます。
- コントローラ:コントローラでは、ユーザーとモデル間の通信を実装します。コントローラは、モデルがデータをどのように処理しているかを認識していませんが、コンテンツを更新する時期であることをモデルに通知できます。一般に、コントローラは、モデル内で何が起こっているのかを理解しようとせずに、インターフェイスによってモデルを操作します。
MVCパターンの個々のコンポーネント間の関係は、次のように視覚的に表すことができます。
それでも、MVCの使用に関して特に厳格なルールや制限はありません。開発者は、モデル操作ロジックをコントローラに追加したり、ビューに干渉したりしないように注意する必要があります。コントローラ自体は軽くしておく必要があります。過負荷にすべきではありません。MVCスキームは、オブザーバーやストラテジーなどの他のデザインパターンにも使用されます。
ここで、MVCテンプレートをMQLで使用する方法と、使用する必要があるかどうかを見てみましょう。
MVCの観点から見た最も単純な指標
最も単純な計算を使用して線を引くことができる単純な指標を作成しましょう。指標は非常に小さく、そのコードは1つのファイルに収まります。以下に示します。
....... #property indicator_chart_window #property indicator_buffers 1 #property indicator_plots 1 //--- plot Label1 #property indicator_label1 "Label1" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDarkSlateBlue #property indicator_style1 STYLE_SOLID #property indicator_width1 2 //--- indicator buffers double lb[]; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0, lb, INDICATOR_DATA); ArraySetAsSeries(lb, true); IndicatorSetString(INDICATOR_SHORTNAME, "Primitive1"); IndicatorSetInteger(INDICATOR_DIGITS, _Digits); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total <= 4) return 0; ArraySetAsSeries(close, true); ArraySetAsSeries(open, true); int limit = rates_total - prev_calculated; if(limit == 0) { } else if(limit == 1) { lb[1] = (open[1] + close[1]) / 2; return(rates_total); } else if(limit > 1) { ArrayInitialize(lb, EMPTY_VALUE); limit = rates_total - 4; for(int i = limit; i >= 1 && !IsStopped(); i--) { lb[i] = (open[i] + close[i]) / 2; } return(rates_total); } lb[0] = (open[0] + close[0]) / 2; return(rates_total); } //+------------------------------------------------------------------+
この指標は、open[i]+close[i]の平均値を計算します。ソースコードは、添付のzipアーカイブMVC_primitive_1.zipにあります。
指標の記述は非常に不十分であり、経験豊富な開発者はそれに簡単に気付くでしょう。open[i]+close[i]ではなくclose[i]のみを使用するように計算メソッドを変更する必要があるようです。この指標には、変更が必要な場所が3つあります。さらに多くの変更を加えたり、計算をより複雑にしたりする必要がある場合はどうなるでしょうか。明らかに、別の関数で計算を実装することをお勧めします。したがって、必要に応じて、この関数でのみ関連する論理修正を行うことができます。
ハンドラと関数は次のようになります。
double Prepare(const datetime &t[], const double &o[], const double &h[], const double &l[], const double &c[], int shift) { ArraySetAsSeries(c, true); ArraySetAsSeries(o, true); return (o[shift] + c[shift]) / 2; } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total <= 4) return 0; int limit = rates_total - prev_calculated; if (limit == 0) { } else if (limit == 1) { lb[1] = Prepare(time, open, high, low, close, 1); return(rates_total); } else if (limit > 1) { ArrayInitialize(lb, EMPTY_VALUE); limit = rates_total - 4; for(int i = limit; i >= 1 && !IsStopped(); i--) { lb[i] = Prepare(time, open, high, low, close, i); } return(rates_total); } lb[0] = Prepare(time, open, high, low, close, 0); return(rates_total); }
ほとんどすべての時系列が新しい関数に渡されることに注意してください。何故でしょうか。始値と終値の2つの時系列のみが使用されるため、これは必須ではありませんが、将来的には、残りの時系列を使用できる指標に多くの変更と改善が行われる可能性があると予想されます。実際、ここで実装しているのは潜在的なバージョンの確固たる基盤です。
次に、MVCテンプレートの観点から現在のコードを考えてみましょう。
- ビュー:このコンポーネントではユーザーにデータを提示するため、指標バッファーに関連するコードが含まれている必要があります。OnInit()のコード(ここでの場合はコード全体)も含める必要があります。
- モデル:指標では、非常に単純な1行モデルで始値と終値の平均を計算し、その後、ビューは自動的に更新されます。したがって、モデルコンポーネントにはPrepare関数のみが含まれます。これは、将来の開発の可能性を念頭に置いて作成されたものです。
- コントローラ:このコンポーネントは、他の2つのコンポーネント間の通信とユーザーの操作を担当します。これによると、コンポーネントにはイベントハンドラと指標入力パラメータが含まれます。また、コントローラは、モデルの入力として機能する準備関数を呼び出します。このような呼び出しは、新しいティックが到着したとき、および銘柄の価格履歴が変更されたときに、モデルの状態を強制的に変更します。
上記の説明に基づいて、指標を再構築してみましょう。コンポーネントのコードをさまざまなファイルだけでなく、さまざまなフォルダにも実装しましょう。複数のビューが存在たりモデルに他のモデルが含まれたりして、コントローラが非常に複雑になる可能性があるため、これは妥当なソリューションです。メイン指標ファイルは次のようになります。
//+------------------------------------------------------------------+ //| MVC_primitive_2.mq5 | //| Copyright 2021, Andrei Novichkov. | //| https://www.mql5.com/en/users/andreifx60/news | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, Andrei Novichkov." #property link "https://www.mql5.com/en/users/andreifx60/news" #property version "1.00" #property indicator_chart_window #property indicator_buffers 1 #property indicator_plots 1 #include "View\MVC_View.mqh" #include "Model\MVC_Model.mqh" //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { return Initialize(); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total <= 4) return 0; int limit = rates_total - prev_calculated; if (limit == 0) { } else if (limit == 1) { lb[1] = Prepare(time, open, high, low, close, 1); return(rates_total); } else if (limit > 1) { ArrayInitialize(lb, EMPTY_VALUE); limit = rates_total - 4; for(int i = limit; i >= 1 && !IsStopped(); i--) { lb[i] = Prepare(time, open, high, low, close, i); } return(rates_total); } lb[0] = Prepare(time, open, high, low, close, 0); return(rates_total); } //+------------------------------------------------------------------+
A few words about the indicator properties:
#property indicator_buffers 1 #property indicator_plots 1
これらの2行は、ビュー(MVC_View.mqhファイル)に移動できますが、コンパイラ警告が生成されます:
no indicator plot defined for indicator
したがって、これらの2行は、コントローラコードを含むメインファイルに残されます。指標のソースコードは、添付のzipアーカイブMVC_primitive_2.zipにあります。
ここでは、パターンの個別のコンポーネント間の通信に注意してください。現在、通信はありません。2つのインクルードファイルを接続するだけで、すべてが機能します。特に、ビューには、グローバル変数の形式の指標バッファーと、初期化が実行される関数が含まれています。この部分をより正確で安全な方法で書き直してみましょう。バッファ、その初期化、およびバッファへのアクセスを1つのオブジェクトにまとめます。これにより、便利でコンパクトなコードが生成され、デバッグと保守が容易になります。さらに、このアプローチは、プログラムをさらに改善するためのすべての機会を提供します。開発者は、コードの一部を基本クラスまたはインターフェイスに移動して、たとえばビューの配列を作成できます。新しいビューは次のようになります。
class CView { public: void CView(); void ResetBuffers(); int Initialize(); void SetData(double value, int shift = 0); private: double _lb[]; int _Width; string _Name; string _Label; };// class CView void CView::CView() { _Width = 2; _Name = "Primitive" ; _Label = "Label1"; }// void CView::CView() void CView::ResetBuffers() { ArrayInitialize(_lb, EMPTY_VALUE); } int CView::Initialize() { SetIndexBuffer (0, _lb, INDICATOR_DATA); ArraySetAsSeries (_lb, true); IndicatorSetString (INDICATOR_SHORTNAME, _Name); IndicatorSetInteger(INDICATOR_DIGITS, _Digits); PlotIndexSetString (0, PLOT_LABEL, _Label); PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE); PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrDarkSlateBlue); PlotIndexSetInteger(0, PLOT_LINE_STYLE, STYLE_SOLID); PlotIndexSetInteger(0, PLOT_LINE_WIDTH, _Width); return(INIT_SUCCEEDED); } void CView::SetData(double value,int shift) { _lb[shift] = value; }
最後のメソッドSetDataに注意してください。ここでは、指標バッファへの制御されていないアクセスを禁止し、アクセスのための特別なメソッドを実装します。これにチェックを追加できます。オプションで、メソッドは基本クラスで仮想として宣言できます。コントローラファイルにもいくつかの小さな変更がありますが、ここでは示していません。明らかに、ここでは、色、スタイルなどのバッファー初期化パラメータを渡すことができる別のコンストラクタが必要です。
また、最初のビューオブジェクトでの呼び出しは適切には思えません。
IndicatorSetString (INDICATOR_SHORTNAME, _Name); IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
実際には、次の点に注意して、CViewクラスから削除する必要があります。
- 多くのビューが存在する可能性がありません。
- これらの2つの文字列は、ビューとは関係ありません。これは指標全体の初期化であるため、コントローラファイルのOnInitハンドラに残します。
この指標のソースコードは、添付のzipアーカイブMVC_primitive_3.zipで提供されています。
メイン指標ファイル(コントローラコードを含むファイル)は大幅に短くなりました。コード全体がより安全になり、将来の変更やデバッグの準備が整っています。しかし、コードが他の開発者にとってより明確かどうかというと、これはかなり疑わしいです。この場合、指標コードをコントローラ、モデル、およびビューを組み合わせた1つのファイルに残す方が合理的です。当初はこのようでした。
この観点は論理的に思えます。ただし、これはこの特定の指標にのみ適用されます。グラフィカルパネルを備え、Web上でデータを要求する、12個のファイルで構成される指標を想像してみてください。この場合、MVCモデルが非常に役立ちます。MVCを使用すると、他の人が理解しやすくなり、エラーを簡単に検出できるようになるだけでなく、潜在的なロジックの変更やその他の変更の基礎が提供されます。特定の開発部分のスペシャリストを招待したい場合、これははるかに簡単になります。別の初期化スキームを追加する必要がある場合、これも実行できます。上記の結論は非常に明白です。プロジェクトが複雑になるほど、MVCパターンはより有用になります。
指標にのみ適用される場合、MVCパターンを使用する可能性の観点からエキスパートアドバイザーの構造を見てみましょう。
エキスパートアドバイザーのMVC
非常に単純な疑似エキスパートアドバイザーを作成しましょう。前のローソク足が強気の場合は買いポジションを開き、弱気の場合は売りポジションを開く必要があります。簡単化のために、EAは実際のポジションを開かずに入口と出口をシミュレートするだけです。市場には1つのポジションしか存在しません。この場合、EAコード(添付のzipアーカイブEa_primitive.mq5)は次のようになります。
datetime dtNow; int iBuy, iSell; int OnInit() { iBuy = iSell = 0; return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { } void OnTick() { if (IsNewCandle() ) { double o = iOpen(NULL,PERIOD_CURRENT,1); double c = iClose(NULL,PERIOD_CURRENT,1); if (c < o) { // Enter Sell if (GetSell() == 1) return; if (GetBuy() == 1) CloseBuy(); EnterSell(); } else { // Enter Buy if (GetBuy() == 1) return; if (GetSell() == 1) CloseSell(); EnterBuy(); } }// if (IsNewCandle() ) }// void OnTick() bool IsNewCandle() { datetime d = iTime(NULL, PERIOD_CURRENT, 0); if (dtNow == -1 || dtNow != d) { dtNow = d; return true; } return false; }// bool IsNewCandle() void CloseBuy() {iBuy = 0;} void CloseSell() {iSell = 0;} void EnterBuy() {iBuy = 1;} void EnterSell() {iSell = 1;} int GetBuy() {return iBuy;} int GetSell() {return iSell;}
指標を検討する際、OnInit、OnDeinitおよびその他のハンドラはコントローラに関連していると既に結論付けています。同じことがエキスパートアドバイザーにも当てはまります。しかし、ビューに何を帰する必要があるのでしょうか。グラフィックオブジェクトやチャートはプロットされません。ご存知のように、ビューはユーザーへのデータ表示を担当します。エキスパートアドバイザーの場合、データ表示とはポジションの表示なので、ビューはポジションに関連するものです。これらには、注文、トレーリングストップ、仮想決済逆指値と決済指値、加重平均価格などが含まれます。
次に、モデルには、ポジションを開き、ロットを選択し、決済逆指値と決済指値を定義することに関する決定ロジックが含まれます。資金管理もモデルに実装する必要があります。これは、価格分析、ボリューム計算、口座状態の確認(おそらく他のサブモデルの状態を確認する)、および結果として生じる市場参入決定など、いくつかのサブモデルで構成されるクローズドシステムのように見え始めます。
上記の考察に従って、疑似EAの構造を変更してみましょう。ロットの計算や口座操作は行われていないため、可能な手順を実行してみましょう。さまざまなコンポーネントに関連する関数をサブフォルダに移動し、それらの一部を編集します。OnTick擬似コードハンドラは次のように変更されます。
void OnTick() { if (IsNewCandle() ) { double o = iOpen(NULL,PERIOD_CURRENT,1); double c = iClose(NULL,PERIOD_CURRENT,1); if (MaySell(o, c) ) EnterSell(); if (MayBuy(o, c) ) EnterBuy(); }// if (IsNewCandle() ) }// void OnTick()
このセクションでも、コードが短くなっていることがわかります。第三者の開発者にとってより明確になっているでしょうか。ここでは、以前に指標について検討した仮定も適用できます。
- EAが複雑になるほど、MVCパターンはより有用になります。
EA全体は、添付のMVC_EA_primitive.zipアーカイブにあります。それでは、MVCパターンを「実際の」コードに適用してみましょう。
これらの目的のために、単純なエキスパートアドバイザーを使用しましょう。必ずしも機能するものや適切に作成されたものである必要はありません。それどころか、エキスパートアドバイザーは適切に記述されていないものにしてください。そうすれば、パターンを使用した場合の効果を評価できます。
この目的のために、2013年に作成された$OrdersInTheMorning EAの古い下書きを見つけました。そのストラテジーは次のとおりです。
- 月曜日の指定された時間に、EAは市場から一定の距離で2つの保留中の売買注文を開始しました。1つの注文がトリガーされると、2番目の注文が削除されました。注文は金曜日の夜に決済されました。指定された通貨ペアのリストでのみ機能しました。
EAはMetaTrader4用に開発されたので、MetaTrader 5用に作り直す必要がありました(これは注意深くは行われませんでした)。元の形式の主なEA関数は次のとおりです。
#property copyright "Copyright 2013, MetaQuotes Software Corp." #property link "http://www.metaquotes.net" #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| script program start function | //+------------------------------------------------------------------+ input double delta = 200; input double volumes = 0.03; input double sTopLossKoeff = 1; input double tAkeProfitKoeff = 2; input int iTHour = 0; input bool bHLprocess = true; input bool oNlyMondeyOrders = false; input string sTimeToCloseOrders = "22:00"; input string sTimeToOpenOrders = "05:05"; input double iTimeIntervalForWork = 0.5; input int iSlippage = 15; input int iTradeCount = 3; input int iTimeOut = 2000; int dg; bool bflag; string smb[] = {"AUDJPY","CADJPY","EURJPY","NZDJPY","GBPJPY","CHFJPY"}; int init () { if ( (iTimeIntervalForWork < 0) || (iTimeIntervalForWork > 24) ) { Alert ("... ",iTimeIntervalForWork); } return (0); } void OnTick() { if ((oNlyMondeyOrders == true) && (DayOfWeek() != 1) ) { } else { int count=ArraySize(smb); bool br = true; for (int i=0; i<count;i++) { if (!WeekOrderParam(smb[i], PERIOD_H4, delta*SymbolInfoDouble(smb[i],SYMBOL_POINT) ) ) br = false; } if (!br) Alert("..."); bflag = true; }//end if if ((oNlyMondeyOrders == true) && (DayOfWeek() != 1) ) else... if ((oNlyMondeyOrders == true) && (DayOfWeek() != 5) ) { } else { if (OrdersTotal() != 0) Alert ("..."); }//end if ((oNlyMondeyOrders == true) && (DayOfWeek() != 5) ) else... } bool WeekOrderParam(string symbol,int tf, double dlt) { int j = -1; datetime mtime = 0; int k = 3; Alert(symbol); if (iTHour >= 0) { if (oNlyMondeyOrders == true) { for (int i = 0; i < k; i++) { mtime = iTime(symbol,0,i); if (TimeDayOfWeek(mtime) == 1) { if (TimeHour(mtime) == iTHour) { j = i; break; } } } } else { for (int i = 0; i < k; i++) { mtime = iTime(symbol,0,i); if (TimeHour(mtime) == iTHour) { j = i; break; } } } if (j == -1) { Print("tf?"); return (false); } }//end if (iTHour >= 0) else j = 0; Alert(j); double bsp,ssp; if (bHLprocess) { bsp = NormalizeDouble(iHigh(symbol,0,j) + dlt, dg); ssp = NormalizeDouble(iLow(symbol,0,j) - dlt, dg); } else { bsp = NormalizeDouble(MathMax(iOpen(symbol,0,j),iClose(symbol,0,j)) + dlt, dg); ssp = NormalizeDouble(MathMin(iOpen(symbol,0,j),iClose(symbol,0,j)) - dlt, dg); } double slsize = NormalizeDouble(sTopLossKoeff * (bsp - ssp), dg); double tpb = NormalizeDouble(bsp + tAkeProfitKoeff*slsize, dg); double tps = NormalizeDouble(ssp - tAkeProfitKoeff*slsize, dg); datetime expr = 0; return (mOrderSend(symbol,ORDER_TYPE_BUY_STOP,volumes,bsp,iSlippage,ssp,tpb,NULL,0,expr,CLR_NONE) && mOrderSend(symbol,ORDER_TYPE_SELL_STOP,volumes,ssp,iSlippage,bsp,tps,NULL,0,expr,CLR_NONE) ); } int mOrderSend( string symbol, int cmd, double volume, double price, int slippage, double stoploss, double takeprofit, string comment = "", int magic=0, datetime expiration=0, color arrow_color=CLR_NONE) { int ticket = -1; for (int i = 0; i < iTradeCount; i++) { // ticket=OrderSend(symbol,cmd,volume,price,slippage,stoploss,takeprofit,comment,magic,expiration,arrow_color); if(ticket<0) Print(symbol,": ",GetNameOP(cmd), GetLastError() ,iTimeOut); else break; } return (ticket); }
初期化ブロック、OnTickハンドラ、ヘルパー関数が存在します。ハンドラはコントローラに残されます。古いinit呼び出しを修正する必要があります。ここで、OnTickに注目してください。ハンドラ内には、いくつかのチェックと、WeekOrderParamヘルパー関数が呼び出されるループがあります。この機能は、市場への参入とポジションを開くことに関する意思決定を指します。このアプローチは絶対に間違っています。ご覧のとおり、関数は長く、マルチネストされた条件とループがあります。この関数は、少なくとも2つの部分に分割する必要があります。最後の関数mOrderSendは非常に優れています。これは、上記のアイデアに基づいてビューを参照します。パターンに応じたEA構造の変更に加えて、コード自体を修正する必要があります。コメントは、関連する変更とともに提供されます。
まず、通貨ペアのリストを入力パラメータに移動します。OnInitハンドラからガベージを削除します。初期化の詳細を含むEA_Init.mqhファイルを作成します。このファイルをメインファイルに接続します。この新しいファイルで、クラスを作成し、その中ですべての初期化を実行します。コードは非常に単純です。
class CInit { public: void CInit(){} void Initialize(string pair); string names[]; double points[]; int iCount; }; void CInit::Initialize(string pair) { iCount = StringSplit(pair, StringGetCharacter(",", 0), names); ArrayResize(points, iCount); for (int i = 0; i < iCount; i++) { points[i] = SymbolInfoDouble(names[i], SYMBOL_POINT); } }
コードは非常に単純ですが、ポイントをいくつか説明します。
- クラスのすべてのメンバーはpublicですが、これはあまり正しくなく、privateメンバーにアクセスするための複数のメソッドでコードが乱雑にならないように例外的に行われています。
- クラスにはメソッドが1つしかないため、必ずしもクラスは必要ありません。ただし、この場合、すべてのデータにグローバルアクセスが含まれるため、回避する必要があります。
- このクラスはユーザとの対話を実装し、コントローラの一部です。
作成したクラスタイプのオブジェクトをメインEAファイルに作成し、その初期化メソッドをOnInitハンドラで呼び出します。
次に、モデルに移ります。OnTickハンドラからすべてのコンテンツを削除します。CModelフォルダとその中のModel.mqhファイルを作成します。新しいファイルにCModelクラスを作成します。クラスには、市場の参入条件と終了条件をチェックするための2つのメソッドが含まれている必要があります。また、このクラスでは、ポジションが開かれたまたは決済されたことを示すフラグを保存する必要があります。このフラグを格納する必要がなければ、クラス全体が存在する必要はないことに注意してください。いくつかの関数で十分です。実際の条件で取引する場合、ボリューム、資金などの追加のチェックを実装する必要があり、それらはすべてモデルに実装する必要があります。モデルを含むファイルは次のようになります。
class CModel { public: void CModel(): bFlag(false) {} bool TimeToOpen(); bool TimeToClose(); private: bool bFlag; }; bool CModel::TimeToOpen() { if (bFlag) return false; MqlDateTime tm; TimeCurrent(tm); if (tm.day_of_week != 1) return false; if (tm.hour < iHourOpen) return false; bFlag = true; return true; } bool CModel::TimeToClose() { if (!bFlag) return false; MqlDateTime tm; TimeCurrent(tm); if (tm.day_of_week != 5) return false; if (tm.hour < iHourClose) return false; bFlag = false; return true; }
前の場合と同様に、メインEAファイルにこのクラスタイプのオブジェクトを作成し、そのメソッド呼び出しをOnInit ハンドラに追加します。
次にビューです。Viewフォルダとその中のView.mqhファイルを作成します。このファイルには、注文とポジションを開く/決済するための要素が含まれています。また、仮想レベル、トレーリングストップ、およびさまざまなグラフィカルオブジェクトを管理するためのコンポーネントもあります。この場合の主な目標は、コードを明確かつシンプルにすることです。オプションとして、クラスを使用せずにビューコンポーネントを実装してみましょう。ビューコンポーネントには3つの機能があります。1つは市場参入、2つ目はすべてのポジションの決済、3つ目は注文決済です。3つの関数はそれぞれ、CTradeタイプのオブジェクトを使用します。このオブジェクトは、使用するたびに新しく作成する必要があります。これは最適ではありません。
void Enter() { CTrade trade; trade.SetExpertMagicNumber(Magic); trade.SetMarginMode(); trade.SetDeviationInPoints(iSlippage); double dEnterBuy, dEnterSell; double dTpBuy, dTpSell; double dSlBuy, dSlSell; double dSlSize; for (int i = 0; i < init.iCount; i++) { dEnterBuy = NormalizeDouble(iHigh(init.names[i],0,1) + delta * init.points[i], _Digits); dEnterSell = NormalizeDouble(iLow(init.names[i],0,1) - delta * init.points[i], _Digits); dSlSell = dEnterBuy; dSlBuy = dEnterSell; dSlSize = (dEnterBuy - dEnterSell) * tAkeProfitKoeff; dTpBuy = NormalizeDouble(dEnterBuy + dSlSize, _Digits); dTpSell = NormalizeDouble(dEnterSell - dSlSize, _Digits); trade.SetTypeFillingBySymbol(init.names[i]); trade.BuyStop(volumes, dEnterBuy, init.names[i], dSlBuy, dTpBuy); trade.SellStop(volumes, dEnterSell, init.names[i], dSlSell, dTpSell); } } void ClosePositions() { CTrade trade; for (int i = PositionsTotal() - 1; i >= 0; i--) { trade.PositionClose(PositionGetTicket(i) ); } } void CloseOrder(string pair) { CTrade trade; ulong ticket; for (int i = OrdersTotal() - 1; i >= 0; i--) { ticket = OrderGetTicket(i); if (StringCompare(OrderGetString(ORDER_SYMBOL), pair) == 0) { trade.OrderDelete(ticket); break; } } }
CViewクラスを作成してコードを変更しましょう。作成済みの関数を新しいクラスに移動し、CTrade型のprivateフィールド用にもう1つのコンポーネント初期化メソッドを作成します。他の場合と同様に、作成したクラスタイプのオブジェクトをメインファイルに作成し、その初期化メソッド呼び出しをOnInitハンドラに追加します。
次に、トリガーされていない注文の削除を実装する必要があります。これを行うには、OnTradeハンドラをコントローラに追加します。ハンドラで、注文数の変更を確認します。変更されている場合は、対応するトリガーされていない注文を削除します。このハンドラは、EAの唯一のトリッキーな部分です。CViewクラスにメソッドを作成し、コントローラのOnTradeハンドラから呼び出します。ビューは次のようになります。
#include <Trade\Trade.mqh> class CView { public: void CView() {} void Initialize(); void Enter(); void ClosePositions(); void CloseAllOrder(); void OnTrade(); private: void InitTicketArray() { ArrayInitialize(bTicket, 0); ArrayInitialize(sTicket, 0); iOrders = 0; } CTrade trade; int iOrders; ulong bTicket[], sTicket[]; }; void CView::OnTrade() { if (OrdersTotal() == iOrders) return; for (int i = 0; i < init.iCount; i++) { if (bTicket[i] != 0 && !OrderSelect(bTicket[i]) ) { bTicket[i] = 0; iOrders--; if (sTicket[i] != 0) { trade.OrderDelete(sTicket[i]); sTicket[i] = 0; iOrders--; } continue; } if (sTicket[i] != 0 && !OrderSelect(sTicket[i]) ) { sTicket[i] = 0; iOrders--; if (bTicket[i] != 0) { trade.OrderDelete(bTicket[i]); bTicket[i] = 0; iOrders--; } } } } void CView::Initialize() { trade.SetExpertMagicNumber(Magic); trade.SetMarginMode(); trade.SetDeviationInPoints(iSlippage); ArrayResize(bTicket, init.iCount); ArrayResize(sTicket, init.iCount); InitTicketArray(); } void CView::Enter() { double dEnterBuy, dEnterSell; double dTpBuy, dTpSell; double dSlBuy, dSlSell; double dSlSize; for (int i = 0; i < init.iCount; i++) { dEnterBuy = NormalizeDouble(iHigh(init.names[i],0,1) + delta * init.points[i], _Digits); dEnterSell = NormalizeDouble(iLow(init.names[i],0,1) - delta * init.points[i], _Digits); dSlSell = dEnterBuy; dSlBuy = dEnterSell; dSlSize = (dEnterBuy - dEnterSell) * tAkeProfitKoeff; dTpBuy = NormalizeDouble(dEnterBuy + dSlSize, _Digits); dTpSell = NormalizeDouble(dEnterSell - dSlSize, _Digits); trade.SetTypeFillingBySymbol(init.names[i]); trade.BuyStop(volumes, dEnterBuy, init.names[i], dSlBuy, dTpBuy); bTicket[i] = trade.ResultOrder(); trade.SellStop(volumes, dEnterSell, init.names[i], dSlSell, dTpSell); sTicket[i] = trade.ResultOrder(); iOrders +=2; } } void CView::ClosePositions() { for (int i = PositionsTotal() - 1; i >= 0; i--) { trade.PositionClose(PositionGetTicket(i) ); } InitTicketArray(); } void CView::CloseAllOrder() { for (int i = OrdersTotal() - 1; i >= 0; i--) { trade.OrderDelete(OrderGetTicket(i)); } }
ご覧のとおり、元のコード全体が書き直されています。改善したことは、間違いありません。作業全体の結果は、添付のEA_Real.zipアーカイブにあります。エキスパートアドバイザー(コントローラ)のメインファイルは次のようになります。
input string smb = "AUDJPY, CADJPY, EURJPY, NZDJPY, GBPJPY, CHFJPY"; input double delta = 200; input double volumes = 0.03; input double tAkeProfitKoeff = 2; input int iHourOpen = 5; input int iHourClose = 22; input int iSlippage = 15; input int Magic = 12345; #include "EA_Init.mqh" #include "View\View.mqh" #include "Model\Model.mqh" CInit init; CModel model; CView view; int OnInit() { init.Initialize(smb); view.Initialize(); return INIT_SUCCEEDED; } void OnTick() { if (model.TimeToOpen() ) { view.Enter(); return; } if (model.TimeToClose() ) { view.CloseAllOrder(); view.ClosePositions(); } } void OnTrade() { view.OnTrade(); }
これで、変更、追加、修正が必要な場合は、エキスパートアドバイザーの適切なコンポーネントを使用するだけで済みます。コンポーネントの位置は簡単に決定できます。EAをさらに開発し、新しい機能を実装し、モデルを追加し、ビューを拡張することができます。他の2つに影響を与えることなく、コンポーネントの1つを完全に変更することもできます。
検討中のMVCアプリケーションには、記事の冒頭で述べた1つの側面があります。それは、パターンコンポーネントの相互作用に関するものです。ユーザの観点からは、問題はありません。ダイアログボックスと取引パネルを追加できるコントローラがあります。コントローラの一部として入力パラメータもあります。しかし、モデルとビューはどのように相互作用する必要があるのでしょうか。これらはエキスパートアドバイザーでは互いに直接対話せず、OnTickハンドラのコントローラを介してのみ対話します。さらに、ビューは同様にCInintオブジェクトメソッドを「直接」呼び出すことによってコントローラと通信します。この場合、コンポーネントの相互作用は、それらのグローバルオブジェクトを介して編成されます。これは、エキスパートアドバイザーが非常に単純であり、コードを複雑にしたくなかったためです。
ただし、コードが単純なのにかかわらず、ビューはコントローラを11回呼び出します。EAをさらに開発すると、相互作用の数が何度も増加し、MVCパターンのプラスの効果が損なわれる可能性があります。この問題は、グローバルオブジェクトを拒否し、参照によってコンポーネントとメソッドにアクセスすることで解決できます。この種の相互作用の例は、MFCとそのドキュメントおよびビューコンポーネントです。
パターンの観点からは、パターンコンポーネント間の相互作用はまったく規制されていません。したがって、このトピックについては詳しく説明しません。
終わりに
結論として、元々MVCパターンを適用した指標またはエキスパートアドバイザーの構造をさらに発展させる方法を見てみましょう。さらに2つのモデルがあるとします。ビューもあと1つあるとします。コントローラははるかに複雑になっています。MVCパターンに固執するにはどうすればよいでしょうか。ここでの解決策は、個別のモジュールを使用することです。これはとても簡単です。3つのコンポーネントがあり、それぞれがそれにアクセスするためのメソッドを提供します。各コンポーネントは、個別のモジュールで構成されています。この方法はこちらで言及されています。同じ記事で、モジュールレベルでの相互作用と管理の方法について説明しました。
以下は本稿で使用されているプログラムです。
# | 公開先 | 種類 | 説明 |
---|---|---|---|
1 | MVC_primitive_1.zip | アーカイブ | 指標の最初かつ最悪のバリエーション。 |
2 | MVC_primitive_2.zip | アーカイブ | コンポーネントに分割された2番目の指標バージョン。 |
3 | MVC_primitive_3.zip | アーカイブ | オブジェクトを含む3番目の指標バージョン。 |
4 | EA_primitive.zip | アーカイブ | 疑似エキスパートアドバイザー |
5 | MVC_EA_primitive.zip | アーカイブ | MVCベースの疑似エキスパートアドバイザー |
6 | EA_Real.zip | アーカイブ | MVCベースのエキスパートアドバイザー |
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/9168





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
AIにわ、素人たので、MT4の残金が出金出来るのであれば、ませかます。また、投資がしたいので、早急に出金出来るよう、お願いします。ご協力に感謝します。