
MQL5での取引戦略の自動化(第5回):Adaptive Crossover RSI Trading Suite戦略の開発
はじめに
前回(第4回)は、Multi-Level Zone Recoveryシステムを紹介し、MetaQuotes Language 5 (MQL5)で複数の独立した取引セットアップを同時に管理するためにZone Recoveryの原則を拡張する方法を示しました。今回(第5回)は、新たにAdaptive Crossover RSI Trading Suite戦略を取り上げます。これは、高確率の取引機会を見極めて実行するために設計された包括的なシステムです。本戦略は、コアシグナル生成器として14期間と50期間の適応移動平均(Adaptive Moving Average)クロスオーバーを用い、確認用フィルターとして14期間の相対力指数(RSI: Relative Strength Indicator)を組み合わせています。
さらに、取引日フィルターを導入することで、低確率の取引セッションを除外し、精度とパフォーマンスの向上を図っています。ユーザビリティ向上のために、チャート上に確認された取引シグナルを矢印で表示し、明確なシグナル説明を注釈として付加しています。また、ダッシュボードを実装し、戦略の状態や主要指標、シグナル活動のリアルタイム概要を提供して、トレーダーが一目で全体状況を把握できるようにしています。この記事では、戦略設計の概要からMQL5での実装、パフォーマンスのバックテスト、結果の分析まで、段階的に解説します。以下の構成で進めます。
- 戦略の設計図
- MQL5での実装
- バックテスト
- 結論
本記事を通じて、適応型フィルターベースの取引システムの作成と、多様な市場環境で堅牢なパフォーマンスを実現するための改良方法を実践的に理解できるようになります。では、さっそく始めましょう。
戦略の設計図
Adaptive Crossover RSI Trading Suite戦略は、移動平均のクロスオーバーとモメンタムの確認を基盤として構築されており、バランスの取れた取引アプローチを実現します。コアシグナルは、14期間の短期移動平均線と50期間の長期移動平均線の交差から導き出されます。短期移動平均線が長期移動平均線を上抜ける現象はゴールデンクロスと呼ばれ、買いシグナルとして強気トレンドの開始を示唆します。一方、短期移動平均線が長期移動平均線を下抜ける現象はデッドクロスと呼ばれ、売りシグナルとして弱気トレンドの開始を示します。
これらのシグナルの精度を高めるために、14期間の相対力指数(RSI)が確認フィルターとして使用されます。RSIは取引のタイミングを市場の勢いと照らし合わせて調整し、買われ過ぎや売られ過ぎの状態でのエントリーを避ける役割を果たします。たとえば、買いシグナルはRSIが50以上のときのみ有効とし、売りシグナルはRSIが50以下のときにのみ成立します。また、取引日フィルターも導入し、歴史的にボラティリティが低い日やパフォーマンスが振るわない日を除外することで、システムのパフォーマンスを最適化します。このフィルターにより、確率の高い取引機会に絞ってエントリーをおこなうことが可能です。まとめると、戦略の設計図は以下のとおりです。
売り取引確認の設計図
買い取引確認の設計図
また、取引が確定すると、システムはチャート上にシグナル矢印と注釈を表示してエントリーポイントを明確に示します。さらに、ダッシュボードではリアルタイムに更新される情報を提供し、シグナルの活動状況、主要な指標、およびシステム全体の状態を一目で把握できるようにします。このような構造化された適応型アプローチにより、戦略は堅牢でありながら使いやすいものとなります。最終的なイメージは以下のようになります。
MQL5での実装
Adaptive Crossover RSI Trading Suite戦略の理論を理解したところで、次はこの理論を自動化し、MetaTrader 5用のMetaQuotes Language 5 (MQL5)を使って、エキスパートアドバイザー(EA)を作成していきましょう。
EAを作成するには、MetaTrader5端末で[ツール]タブをクリックし、[MetaQuotes言語エディタ]を選択するか、キーボードのF4を押します。または、ツールバーのIDE(統合開発環境)アイコンをクリックすることもできます。これにより、MetaQuotes言語エディタ環境が開き、取引ロボット、テクニカルインジケーター、スクリプト、関数のライブラリを作成できるようになります。MetaEditorを開いたら、ツールバーの[ファイル]タブで[新しいファイル]を選択するか、CTRL+Nキーを押して新規ドキュメントを開きます。または、[ツール]タブの新規アイコンをクリックすることもできます。MQLウィザードのポップアップが表示されます。
ウィザードが表示されたら、[EA(テンプレート)]を選択し、[次へ]をクリックします。EAの一般プロパティで、[名前]フィールドにEAのファイル名を入力します。フォルダが存在しない場合にフォルダを指定または作成するには、EA名の前にバックスラッシュを使用することに注意してください。例えば、ここではデフォルトで「Experts\」となっています。つまり、私たちのEAはExpertsフォルダに作成され、そこで見つけることができます。他のフィールドはごく簡単ですが、ウィザードの一番下にあるリンクから、正確な手順を知ることができます。
希望するEAファイル名を入力した後、[次へ]をクリックし、[完了]をクリックします。ここまでくれば、あとはコードを書いて戦略をプログラムするだけです。
まず、EAに関するメタデータを定義することから始めます。これには、EA名、著作権情報、MetaQuotesWebサイトへのリンクが含まれます。EAのバージョンも指定し、1.00とします。
//+------------------------------------------------------------------+ //| Adaptive Crossover RSI Trading Suite.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Forex Algo-Trader, Allan" #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property description "EA that trades based on MA Crossover, RSI + Day Filter" #property strict
これにより、プログラムをロードするときにシステムメタデータが表示されます。次に、プログラム内で使用するグローバル変数をいくつか追加します。まず、ソースコードの先頭で#includeを使用して、取引インスタンスをインクルードします。これにより、CTradeクラスにアクセスできるようになります。これを使用して、取引オブジェクトを作成します。これは取引を開始するために必要なので、非常に重要です。
#include <Trade/Trade.mqh>
CTrade obj_Trade;
プリプロセッサは、#include<Trade/Trade.mqh>行をファイルTrade.mqhの内容に置き換えます。角括弧は、Trade.mqhファイルが標準ディレクトリ(通常、terminal_installation_directory\MQL5\Include)から取得されることを示します。カレントディレクトリは検索に含まれません。この行はプログラム中のどこにでも配置できますが、通常は、より良いコード構造と参照を容易にするために、すべてのインクルージョンはソースコードの先頭に置かれます。CTradeクラスのobj_Tradeオブジェクトを宣言すると、MQL5開発者のおかげで、そのクラスに含まれるメソッドに簡単にアクセスできるようになります。
次に、ユーザーがコードを変更せずに取引パラメータを自由に設定できるよう、いくつかの重要な入力変数を宣言します。そのために、入力項目を「一般(general)」「インジケーター(indicator)」「フィルター(filter)」の設定グループに分けて整理し、わかりやすく管理します。
sinput group "GENERAL SETTINGS" sinput double inpLots = 0.01; // LotSize input int inpSLPts = 300; // Stoploss Points input double inpR2R = 1.0; // Risk to Reward Ratio sinput ulong inpMagicNo = 1234567; // Magic Number input bool inpisAllowTrailingStop = true; // Apply Trailing Stop? input int inpTrailPts = 50; // Trailing Stop Points input int inpMinTrailPts = 50; // Minimum Trailing Stop Points sinput group "INDICATOR SETTINGS" input int inpMA_Fast_Period = 14; // Fast MA Period input ENUM_MA_METHOD inpMA_Fast_Method = MODE_EMA; // Fast MA Method input int inpMA_Slow_Period = 50; // Slow MA Period input ENUM_MA_METHOD inpMA_Slow_Method = MODE_EMA; // Slow MA Method sinput group "FILTER SETTINGS" input ENUM_TIMEFRAMES inpRSI_Tf = PERIOD_CURRENT; // RSI Timeframe input int inpRSI_Period = 14; // RSI Period input ENUM_APPLIED_PRICE inpRSI_Applied_Price = PRICE_CLOSE; // RSI Application Price input double inpRsiBUYThreshold = 50; // BUY Signal Threshold input double inpRsiSELLThreshold = 50; // SELL Signal Threshold input bool Sunday = false; // Trade on Sunday? input bool Monday = false; // Trade on Monday? input bool Tuesday = true; // Trade on Tuesday? input bool Wednesday = true; // Trade on Wednesday? input bool Thursday = true; // Trade on Thursday? input bool Friday = false; // Trade on Friday? input bool Saturday = false; // Trade on Saturday?
ここでは、Adaptive Crossover RSI Trading Suiteプログラムの主要なパラメータと設定を定義し、その動作を正確に制御できるようにします。設定は「GENERAL SETTINGS」、「INDICATOR SETTINGS」、「FILTER SETTINGS」という3つの主要なグループに分け、さらに取引日ごとの制御もおこないます。変数の型や列挙体を活用することで、システムの設計に柔軟性と分かりやすさを持たせています。
「GENERAL SETTINGS」グループでは、取引管理に関するパラメータを定義します。最適化可能なパラメータにはキーワード「input」を使用し、文字列や最適化不可のパラメータにはキーワード「sinput」を使用します。たとえば、変数「inpLots」は取引のロットサイズを指定し、「inpSLPts」はストップロスのポイント数を設定して、各取引のリスク管理をおこないます。リスク対リワード比率は「inpR2R」で設定し、リスクと利益のバランスを保ちます。一意の取引識別子は「inpMagicNo」で割り当てられ、プログラムはこれを使って自身の注文を区別します。トレーリングストップ機能は「inpisAllowTrailingStop」で有効・無効を切り替えられ、「inpTrailPts」と「inpMinTrailPts」でそれぞれトレーリング距離と発動の最低ポイント数を指定し、市場環境に応じた適切な動作を実現します。
「INDICATOR SETTINGS」グループでは、シグナル生成の基盤となる移動平均のパラメータを設定します。短期移動平均の期間は「inpMA_Fast_Period」で指定し、その計算方法は列挙体ENUM_MA_METHOD型の「inpMA_Fast_Method」で選択します。選択肢にはMODE_SMA、MODE_EMA、MODE_SMMA、MODE_LWMAなどがあります。同様に、長期移動平均は「inpMA_Slow_Period」で期間を設定し、計算方法は「inpMA_Slow_Method」で指定します。これらの列挙体を使うことで、ユーザーは異なる市場環境に応じて好みの移動平均の種類を柔軟に選択できます。
「FILTER SETTINGS」グループでは、モメンタムフィルターとして機能するRSIインジケーターのパラメータを定義します。RSIの時間枠はENUM_TIMEFRAMES型の変数「inpRSI_Tf」で選択でき、PERIOD_M1、PERIOD_H1、PERIOD_D1などを指定できます。RSIの期間は「inpRSI_Period」で設定し、計算に使う価格データはENUM_APPLIED_PRICE型の「inpRSI_Applied_Price」で指定します(例:PRICE_CLOSE、PRICE_OPEN、PRICE_MEDIANなど)。さらに、RSIの買い・売りシグナルを検証するための閾値は「inpRsiBUYThreshold」と「inpRsiSELLThreshold」で設定し、市場のモメンタムと一致したシグナルのみを有効化します。
最後に、取引日フィルターを実装し、曜日ごとにブール型の変数(「Sunday」、「Monday」など)でEAの稼働を制御します。これにより、歴史的にボラティリティが低い日やパフォーマンスが悪い日を避け、無駄なリスクを軽減します。以上の設定が完了した後、次に使用するインジケーターハンドルを定義します。
int handleMAFast = INVALID_HANDLE; int handleMASlow = INVALID_HANDLE; int handleRSIFilter = INVALID_HANDLE;
3つの主要な変数「handleMAFast」、「handleMASlow」、「handleRSIFilter」を初期化し、それぞれをINVALID_HANDLEに設定します。これにより、EAがクリーンかつ制御された状態で起動し、未初期化または無効なインジケーターハンドルによる問題を回避できます。「handleMAFast」は短期移動平均インジケーターの管理に使用し、設定したパラメータに基づいて短期的な価格動向を捉えられるように構成します。
同様に、「handleMASlow」は長期移動平均インジケーターを管理し、長期的な価格動向を追跡します。これらのハンドルは、戦略に必要な移動平均の値を動的に取得・処理するために不可欠です。そして「handleRSIFilter」は、シグナルの確認に用いるモメンタムフィルターとしてのRSIインジケーターに接続するための準備をおこないます。次に、これらのインジケーターから取得したデータを格納するための配列を定義する必要があります。これには3つの配列が必要です。
double bufferMAFast[]; double bufferMASlow[]; double bufferRSIFilter[];
ここでは、3つの動的配列「bufferMAFast[]」、「bufferMASlow[]」、「bufferRSIFilter[]」を宣言します。これらの配列は、戦略で使用するインジケーターの計算結果を格納・管理するためのストレージとして機能します。このようにデータを整理することで、EAは動作中にインジケーターの結果へ直接かつ効率的にアクセスできるようになります。これ以降は、初期化関数に移り、インジケーターハンドルを作成する必要があります。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- handleMAFast = iMA(_Symbol,_Period,inpMA_Fast_Period,0,inpMA_Fast_Method,PRICE_CLOSE); handleMASlow = iMA(_Symbol,_Period,inpMA_Slow_Period,0,inpMA_Slow_Method,PRICE_CLOSE); handleRSIFilter = iRSI(_Symbol,inpRSI_Tf,inpRSI_Period,inpRSI_Applied_Price); //---- }
ここでは、戦略で使用するインジケーターのハンドルを初期化します。対象は、短期移動平均、長期移動平均、そしてRSIフィルターです。まず、「iMA」関数を使って短期移動平均を初期化します。この関数には複数のパラメータが必要です。最初のパラメータ「_Symbol」は、現在の取引銘柄の移動平均を計算することを指定します。次に「_Period」はチャートの時間足(例:1分足、1時間足)を指定します。
さらに、短期移動平均の期間である「inpMA_Fast_Period」を渡します。これは移動平均計算に用いるバーの本数です。パラメータ「0」は移動平均のシフトを示し、「0」はシフトなしを意味します。「inpMA_Fast_Method」は移動平均の計算方法(たとえば指数移動平均や単純移動平均)を指定し、「PRICE_CLOSE」は各バーの終値を使って平均を算出することを表します。
この関数の戻り値は「handleMAFast」に代入され、後の計算で短期移動平均の値にアクセスできるようになります。
次に、同じく「iMA」関数を使って長期移動平均を初期化します。こちらも「_Symbol」、「_Period」、「inpMA_Slow_Period」を使い、計算方法と価格種別(「PRICE_CLOSE」)を指定します。この戻り値は「handleMASlow」に格納され、後続処理で使用します。最後に、「iRSI」関数でRSIフィルターを初期化します。ここでは銘柄を指定する「_Symbol」、RSIの時間足「inpRSI_Tf」、RSI期間「inpRSI_Period」、および適用価格「inpRSI_Applied_Price」を引数に渡します。関数の戻り値は「handleRSIFilter」に格納され、戦略のシグナル確認でRSI値を利用できるようになります。
これらのハンドルは戦略の基盤となるため、正しく初期化されていることが必須です。もし初期化に失敗した場合は、プログラムの実行を続ける意味がありません。
if (handleMAFast == INVALID_HANDLE || handleMASlow == INVALID_HANDLE || handleRSIFilter == INVALID_HANDLE){ Print("ERROR! Unable to create the indicator handles. Reveting Now!"); return (INIT_FAILED); }
ここでは、インジケーターハンドルの初期化が正常におこなわれたかどうかを確認します。具体的には、「handleMAFast」、「handleMASlow」、「handleRSIFilter」のいずれかがINVALID_HANDLEに等しいかどうかをチェックします。これらのいずれかが無効であれば、対応するインジケーターの生成に失敗したことを意味します。もし1つでもハンドルの取得に失敗していれば、「Print」関数を使ってターミナルにエラーメッセージを表示し、問題をユーザーに通知します。さらに、INIT_FAILEDを返すことで、EAの実行を中止します。これにより、誤った状態でEAが動作を継続しないようにします。
また、もう一つの不具合として、ユーザーが非現実的な期間を指定してしまうケースがあります。たとえば、移動平均やRSIの期間として0以下の値が入力された場合です。これを防ぐために、短期移動平均(inpMA_Fast_Period)、長期移動平均(inpMA_Slow_Period)、およびRSI (inpRSI_Period)の各期間がすべて1以上の正の値であることを確認するチェック処理も必要です。
if (inpMA_Fast_Period <= 0 || inpMA_Slow_Period <= 0 || inpRSI_Period <= 0){ Print("ERROR! Periods cannot be <= 0. Reverting Now!"); return (INIT_PARAMETERS_INCORRECT); }
ここでは、ユーザーが入力した期間の値が0以下であった場合、INIT_PARAMETERS_INCORRECTを返してプログラムを終了します。このチェックを通過すれば、インジケーターハンドルが正しく初期化されたことになり、次にストレージ配列を時系列として設定する準備が整います。
ArraySetAsSeries(bufferMAFast,true); ArraySetAsSeries(bufferMASlow,true); ArraySetAsSeries(bufferRSIFilter,true); obj_Trade.SetExpertMagicNumber(inpMagicNo); Print("SUCCESS INITIALIZATION. ACCOUNT TYPE = ",trading_Account_Mode());
最後に、初期化プロセスを完了するために、いくつかの重要な処理をおこないます。まず、「ArraySetAsSeries」関数を使用して、bufferMAFast、bufferMASlow、およびbufferRSIFilterの各配列を時系列として設定します。これは、これらの配列に格納されるデータが、MetaTraderでの時系列データの扱い方(最新のデータがインデックス0に格納される)と互換性を持つようにするために重要です。各配列を時系列として設定することで、取引ロジック内でインジケーターのデータに正しい順序でアクセスできるようになります。
次に、obj_Tradeオブジェクトの「SetExpertMagicNumber」メソッドを呼び出し、「inpMagicNo」の値をマジックナンバーとして渡します。マジックナンバーは、EAによって発行された取引を一意に識別するための番号であり、手動での取引や他のEAによる取引と区別するために使用されます。最後に、「Print」関数を使って、ターミナルに初期化の完了を知らせる成功メッセージを出力します。このメッセージには、「trading_Account_Mode」関数を使用して取得される口座の種類(デモ口座かライブ口座か)が含まれます。これを担う関数は以下のとおりです。
string trading_Account_Mode(){ string account_mode; switch ((ENUM_ACCOUNT_TRADE_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE)){ case ACCOUNT_TRADE_MODE_DEMO: account_mode = "DEMO"; break; case ACCOUNT_TRADE_MODE_CONTEST: account_mode = "COMPETITION"; break; case ACCOUNT_TRADE_MODE_REAL: account_mode = "REAL"; break; } return account_mode; }
ここでは、取引口座の種類(デモ口座、コンテスト口座、またはリアル口座)を判定するために、文字列関数「trading_Account_Mode」を定義します。この関数は、ACCOUNT_TRADE_MODEパラメータの値に基づいて、EAが接続されている口座の種類を判別します。まず、口座の種類を文字列として格納するための変数「account_mode」を宣言します。次に、switch文を使用して口座の取引モードを評価します。このモードは、「AccountInfoInteger」関数にACCOUNT_TRADE_MODEを渡すことで取得され、整数値として返されます。switch文では、この整数値をチェックし、以下のいずれかのモードに一致するかを判定します。
- 口座モードがACCOUNT_TRADE_MODE_DEMOの場合、「account_mode」を「DEMO」に設定します。
- 口座モードがACCOUNT_TRADE_MODE_CONTESTの場合、「account_mode」を「COMPETITION」に設定します。
- 口座モードがACCOUNT_TRADE_MODE_REALの場合、「account_mode」を「REAL」に設定します。
最後に、この関数はaccount_modeを文字列として返します。これにより、EAがどの種類の口座に接続されているかを明示的に判別することができます。したがって、最終的な初期化関数は次のようになります。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- handleMAFast = iMA(_Symbol,_Period,inpMA_Fast_Period,0,inpMA_Fast_Method,PRICE_CLOSE); handleMASlow = iMA(_Symbol,_Period,inpMA_Slow_Period,0,inpMA_Slow_Method,PRICE_CLOSE); handleRSIFilter = iRSI(_Symbol,inpRSI_Tf,inpRSI_Period,inpRSI_Applied_Price); if (handleMAFast == INVALID_HANDLE || handleMASlow == INVALID_HANDLE || handleRSIFilter == INVALID_HANDLE){ Print("ERROR! Unable to create the indicator handles. Reveting Now!"); return (INIT_FAILED); } if (inpMA_Fast_Period <= 0 || inpMA_Slow_Period <= 0 || inpRSI_Period <= 0){ Print("ERROR! Periods cannot be <= 0. Reverting Now!"); return (INIT_PARAMETERS_INCORRECT); } ArraySetAsSeries(bufferMAFast,true); ArraySetAsSeries(bufferMASlow,true); ArraySetAsSeries(bufferRSIFilter,true); obj_Trade.SetExpertMagicNumber(inpMagicNo); Print("SUCCESS INITIALIZATION. ACCOUNT TYPE = ",trading_Account_Mode()); //--- return(INIT_SUCCEEDED); }
ここで、OnDeinitイベントハンドラに進みます。ここでは、インジケーターハンドルはもう必要ないので、解放する必要があります。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- IndicatorRelease(handleMAFast); IndicatorRelease(handleMASlow); IndicatorRelease(handleRSIFilter); }
インジケータハンドルに割り当てられたリソースを解放するには、まず、各インジケーターハンドル(handleMAFast、handleMASlow、handleRSIFilter)に対して関数「IndicatorRelease」を呼び出します。この関数の目的は、EAの実行中に初期化されたインジケーターハンドルに関連付けられたメモリやリソースを解放することです。これにより、使用されなくなったインジケーターによってプラットフォームのリソースが無駄に占有されるのを防ぎます。次に、OnTickイベントハンドラへと進みます。ここは、EAの主要な取引ロジックが実行される場所です。まず最初に、インジケーターハンドルからインジケーターデータを取得する必要があります。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- Check if data can be retrieved for the fast moving average (MA) if (CopyBuffer(handleMAFast,0,0,3,bufferMAFast) < 3){ //--- Print error message for fast MA data retrieval failure Print("ERROR! Failed to retrieve the requested FAST MA data. Reverting."); //--- Exit the function if data retrieval fails return; } //--- Check if data can be retrieved for the slow moving average (MA) if (CopyBuffer(handleMASlow,0,0,3,bufferMASlow) < 3){ //--- Print error message for slow MA data retrieval failure Print("ERROR! Failed to retrieve the requested SLOW MA data. Reverting."); //--- Exit the function if data retrieval fails return; } //--- Check if data can be retrieved for the RSI filter if (CopyBuffer(handleRSIFilter,0,0,3,bufferRSIFilter) < 3){ //--- Print error message for RSI data retrieval failure Print("ERROR! Failed to retrieve the requested RSI data. Reverting."); //--- Exit the function if data retrieval fails return; } //--- }
ここでは、EAが適切な取引判断をおこなうために必要な、短期移動平均線、長期移動平均線、およびRSIフィルターの最新のインジケーターデータを取得することに焦点を当てます。まず、handleMAFast(短期移動平均線のハンドル)を対象に、CopyBuffer関数を使用してインジケーターデータを取得します。この関数は、対応するバッファ(bufferMAFast)にインジケーターの値を格納します。具体的には、チャート上で最も新しいデータを示すインデックス0から、3本分のデータを要求します。もし取得されたデータの数が3未満であれば、必要なデータの取得に失敗したことを意味します。この場合、Print関数でエラーメッセージを出力し、return文で関数を即座に終了します。
次に、同様の処理をhandleMASlow(長期移動平均線のハンドル)と対応するバッファ(bufferMASlow)に対しておこないます。この場合も、CopyBufferが3本以上のデータポイントを取得できなかった場合は、エラーメッセージを出力し、関数を終了させます。最後に、RSIフィルターのハンドル(handleRSIFilter)とバッファ(bufferRSIFilter)に対して、同様に「CopyBuffer」関数を使用します。これまでと同様に、要求されたデータポイントが正常に取得されたことを確認します。それ以外の場合は、エラーメッセージを出力し、関数を終了させます。この時点までで関数が終了していなければ、EAは取引シグナルを生成するために必要な情報をすべて備えていることになります。ただし、すべてのティックでシグナルを生成するのではなく、新しいバーが形成されたときにのみシグナルを出したいと考えているため、新しいバーが生成されたかどうかを検出する関数が必要です。
//+------------------------------------------------------------------+ //| Function to detect if a new bar is formed | //+------------------------------------------------------------------+ bool isNewBar(){ //--- Static variable to store the last bar count static int lastBarCount = 0; //--- Get the current bar count int currentBarCount = iBars(_Symbol,_Period); //--- Check if the bar count has increased if (currentBarCount > lastBarCount){ //--- Update the last bar count lastBarCount = currentBarCount; //--- Return true if a new bar is detected return true; } //--- Return false if no new bar is detected return false; }
ここでは、チャート上に新しいバーが出現したかどうかを検出するための「isNewBar」関数を定義します。この関数は、すべてのティックごとに処理をおこなうのではなく、1バーにつき1回だけ処理を実行するために重要な役割を果たします。まず、「lastBarCount」というstatic変数を宣言し、初期値を0に設定します。static変数は関数が呼び出されるたびに再初期化されることなく、前回の呼び出し時の値を保持するため、状態の比較に適しています。次に、iBars関数を使って、チャート上のバーの本数を取得します。この関数には、現在の通貨ペア(_Symbol)と現在の時間軸(_Period)を渡し、取得した結果をcurrentBarCountに格納します。
その後、currentBarCountとlastBarCountを比較します。currentBarCountがlastBarCountより大きければ、新しいバーが形成されたことを意味します。この場合、lastBarCountをcurrentBarCountに更新し、trueを返して新しいバーの存在を示します。それ以外の場合は、新しいバーは生成されていないため、falseを返します。このようにして、OnTickイベントハンドラ内でこの関数を使用することができます。
//--- Check if a new bar has formed if (isNewBar()){ //--- Print debug message for a new tick //Print("THIS IS A NEW TICK"); //--- Identify if a buy crossover has occurred bool isMACrossOverBuy = bufferMAFast[1] > bufferMASlow[1] && bufferMAFast[2] <= bufferMASlow[2]; //--- Identify if a sell crossover has occurred bool isMACrossOverSell = bufferMAFast[1] < bufferMASlow[1] && bufferMAFast[2] >= bufferMASlow[2]; //--- Check if the RSI confirms a buy signal bool isRSIConfirmBuy = bufferRSIFilter[1] >= inpRsiBUYThreshold; //--- Check if the RSI confirms a sell signal bool isRSIConfirmSell = bufferRSIFilter[1] <= inpRsiSELLThreshold; //--- }
ここでは、移動平均線の関係性とRSIによるフィルタリングに基づいて、特定の売買シグナルを検出するためのコアロジックを実装します。まず、「isNewBar」関数を使用して新しいバーが形成されたかどうかを確認します。これにより、同じバー内での繰り返し評価を避け、1バーにつき1回だけ処理を実行できるようになります。
新しいバーが検出された場合、まず買いのゴールデンクロス(買いシグナル)を識別するために、短期移動平均と長期移動平均の関係を評価します。具体的には、1本前のバーで短期移動平均(bufferMAFast[1])が長期移動平均(bufferMASlow[1])を上回っており、2本前のバーでは短期移動平均(bufferMAFast[2])が長期移動平均(bufferMASlow[2])以下だった場合に、上抜けクロス(ゴールデンクロス)が発生したとみなします。両方の条件が真の場合、ブール変数「isMACrossOverBuy」をtrueに設定し、買いシグナルの発生を示します。
同様に、売りのデッドクロス(売りシグナル)を識別するには、1本前のバーで短期移動平均(bufferMAFast[1])が長期移動平均(bufferMASlow[1])を下回っており、2本前のバーでは短期移動平均(bufferMAFast[2])が長期移動平均(bufferMASlow[2])以上であった場合、下抜けクロス(デッドクロス)と判断します。この場合は、ブール変数「isMACrossOverSell」をtrueに設定し、売りシグナルの発生を示します。
次に、検出されたクロスをRSIによってフィルタリングします。買いの確認には、1本前のRSI値(bufferRSIFilter[1])が設定された買いの閾値(inpRsiBUYThreshold)以上であることを確認し、条件を満たせば、ブール変数「isRSIConfirmBuy」をtrueに設定します。同様に、売りの確認には、1本前のRSI値(bufferRSIFilter[1])が売りの閾値(inpRsiSELLThreshold)以下であることを確認し、条件が成立すれば、ブール変数「isRSIConfirmSell」をtrueに設定します。これらのブール変数を用いることで、売買の意思決定を実行することができるようになります。
//--- Handle buy signal conditions if (isMACrossOverBuy){ if (isRSIConfirmBuy){ //--- Print buy signal message Print("BUY SIGNAL"); //--- }
ここでは、移動平均線が交差しているかどうかを確認し、rsiがシグナルを確認します。すべての条件が満たされている場合は、買いシグナルを出力します。ただし、買いポジションを開く前に、取引日フィルターの遵守を確認する必要があります。したがって、すべてをモジュール化したままにする関数が必要です。
//+------------------------------------------------------------------+ //| Function to check trading days filter | //+------------------------------------------------------------------+ bool isCheckTradingDaysFilter(){ //--- Structure to store the current date and time MqlDateTime dateTIME; //--- Convert the current time into structured format TimeToStruct(TimeCurrent(),dateTIME); //--- Variable to store the day of the week string today = "DAY OF WEEK"; //--- Assign the day of the week based on the numeric value if (dateTIME.day_of_week == 0){today = "SUNDAY";} if (dateTIME.day_of_week == 1){today = "MONDAY";} if (dateTIME.day_of_week == 2){today = "TUESDAY";} if (dateTIME.day_of_week == 3){today = "WEDNESDAY";} if (dateTIME.day_of_week == 4){today = "THURSDAY";} if (dateTIME.day_of_week == 5){today = "FRIDAY";} if (dateTIME.day_of_week == 6){today = "SATURDAY";} //--- Check if trading is allowed based on the input parameters if ( (dateTIME.day_of_week == 0 && Sunday == true) || (dateTIME.day_of_week == 1 && Monday == true) || (dateTIME.day_of_week == 2 && Tuesday == true) || (dateTIME.day_of_week == 3 && Wednesday == true) || (dateTIME.day_of_week == 4 && Thursday == true) || (dateTIME.day_of_week == 5 && Friday == true) || (dateTIME.day_of_week == 6 && Saturday == true) ){ //--- Print acceptance message for trading Print("Today is on ",today,". Trade ACCEPTED."); //--- Return true if trading is allowed return true; } else { //--- Print rejection message for trading Print("Today is on ",today,". Trade REJECTED."); //--- Return false if trading is not allowed return false; } }
ここでは、「isCheckTradingDaysFilter」という関数を作成し、ユーザーが設定した入力値に基づいて現在の曜日に取引を許可するかどうかを判定します。この機能により、許可された取引日のみに取引が実行されるようになり、不要または意図しない曜日での取引を回避できます。まず、現在の日時を格納するためのMqlDateTime構造体オブジェクト「dateTIME」を定義します。「TimeToStruct」関数を使って、現在のサーバー時刻(TimeCurrent)をこの構造体に変換することで、曜日などの要素を個別に参照できるようにします。
次に、現在の曜日名を文字列で格納するための変数「today」を定義し、初期値として「DAY OF WEEK」というプレースホルダーを代入します。これにより、現在の曜日の名前が人間が読める形式で後で保存されます。その後、一連のif条件を使用して、構造体内のday_of_week(数値:日曜=0 ~ 土曜=6)を、対応する曜日名の文字列にマッピングし、変数「today」に代入します。
続いて、現在の曜日が取引許可されているかを判定します。これは、「dateTIME.day_of_week」の値と、ユーザーが指定した各曜日のブール型入力(「Sunday」、「Monday」、...)とを照らし合わせて確認します。現在の曜日が許可されている場合は、「Print」関数を使って「取引可能」である旨のメッセージ(曜日名付き)を出力し、関数はtrueを返します。一方で、取引が許可されていない曜日の場合は、「取引拒否」のメッセージを出力し、関数はfalseを返します。このように、本関数はユーザーが指定した曜日ごとの取引可否設定に基づいて処理を制御するゲートキーパーの役割を果たします。この関数を活用して取引可能日のフィルターを実装することができます。
//--- Verify trading days filter before placing a trade if (isCheckTradingDaysFilter()){ //--- Retrieve the current ask price double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); //--- Retrieve the current bid price double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- Set the open price to the ask price double openPrice = Ask; //--- Calculate the stop-loss price double stoploss = Bid - inpSLPts*_Point; //--- Calculate the take-profit price double takeprofit = Bid + (inpSLPts*inpR2R)*_Point; //--- Define the trade comment string comment = "BUY TRADE"; //--- Execute a buy trade obj_Trade.Buy(inpLots,_Symbol,openPrice,stoploss,takeprofit,comment); //--- Initialize the ticket variable ulong ticket = 0; //--- Retrieve the order result ticket ticket = obj_Trade.ResultOrder(); //--- Print success message if the trade is opened if (ticket > 0){ Print("SUCCESS. Opened the BUY position with ticket # ",ticket); } //--- Print error message if the trade fails to open else {Print("ERROR! Failed to open the BUY position.");} }
ここでは、「isCheckTradingDaysFilter」関数を呼び出して、本日が取引を許可された曜日かどうかを確認します。この関数がtrueを返した場合、市場データの取得および取引注文の実行へと進み、ユーザーが指定した曜日フィルターに従った取引がおこなわれるようになります。まず、SymbolInfoDouble関数を使って、現在の市場価格を取得します。パラメータ「SYMBOL_ASK」と「SYMBOL_BID」を指定することで、アクティブな通貨ペア(_Symbol)における現在のAsk価格とBid価格を取得し、それぞれ変数「Ask」および「Bid」に格納します。これらは、今後の計算処理の基準価格となります。
次に、実際の注文に必要な価格を計算します。Buyポジションの場合、Ask価格をエントリープライス(openPrice)として設定します。ストップロス価格は、Bid価格から「inpSLPts × _Point」を引いた値として計算されます。テイクプロフィット価格は、Bid価格に「inpSLPts × inpR2R × _Point」を加えることで求められます。これらの価格は、リスクとリワードの境界線を定義するための重要な要素です。
その後、将来的に注文を識別するためのコメント(「BUY TRADE」)を定義します。続いて、「obj_Trade.Buy」メソッドを使用して買い注文を実行します。このメソッドには、ロット数(inpLots)、取引銘柄、エントリー価格(openPrice)、ストップロス価格、テイクプロフィット価格、コメントをパラメータとして渡します。この関数は、実際に市場に注文を送信する役割を果たします。取引の実行後、まずticket変数を0に初期化し、その後obj_Trade.ResultOrderメソッドを使用して返された注文チケット番号をticketに代入します。チケット番号が0より大きければ、注文が正常に発注されたことを示しており、チケット番号付きの成功メッセージをPrint関数で出力します。一方で、チケットが0のままであれば、注文が失敗したことを意味し、エラーメッセージが出力されます。売り注文の場合は、同じ手順に従いますが、条件や価格の使い方が逆方向になります。コードスニペットは次のとおりです。
//--- Handle sell signal conditions else if (isMACrossOverSell){ if (isRSIConfirmSell){ //--- Print sell signal message Print("SELL SIGNAL"); //--- Verify trading days filter before placing a trade if (isCheckTradingDaysFilter()){ //--- Retrieve the current ask price double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); //--- Retrieve the current bid price double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- Set the open price to the bid price double openPrice = Bid; //--- Calculate the stop-loss price double stoploss = Ask + inpSLPts*_Point; //--- Calculate the take-profit price double takeprofit = Ask - (inpSLPts*inpR2R)*_Point; //--- Define the trade comment string comment = "SELL TRADE"; //--- Execute a sell trade obj_Trade.Sell(inpLots,_Symbol,openPrice,stoploss,takeprofit,comment); //--- Initialize the ticket variable ulong ticket = 0; //--- Retrieve the order result ticket ticket = obj_Trade.ResultOrder(); //--- Print success message if the trade is opened if (ticket > 0){ Print("SUCCESS. Opened the SELL position with ticket # ",ticket); } //--- Print error message if the trade fails to open else {Print("ERROR! Failed to open the SELL position.");} } } }
プログラムを実行すると、次の結果が得られます。
画像から、取引が確認されていることが分かります。しかし、シグナルを視覚的にチャート上に表示することで、より分かりやすくなるでしょう。そのため、矢印と注釈を描画する関数が必要になります。
//+------------------------------------------------------------------+ //| Create signal text function | //+------------------------------------------------------------------+ void createSignalText(datetime time,double price,int arrowcode, int direction,color clr,double angle,string txt ){ //--- Generate a unique name for the signal object string objName = " "; StringConcatenate(objName, "Signal @ ",time," at Price ",DoubleToString(price,_Digits)); //--- Create the arrow object at the specified time and price if (ObjectCreate(0,objName,OBJ_ARROW,0,time,price)){ //--- Set arrow properties ObjectSetInteger(0,objName,OBJPROP_ARROWCODE,arrowcode); ObjectSetInteger(0,objName,OBJPROP_COLOR,clr); if (direction > 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_TOP); if (direction < 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_BOTTOM); } //--- Generate a unique name for the description text object string objNameDesc = objName+txt; //--- Create the text object at the specified time and price if (ObjectCreate(0,objNameDesc,OBJ_TEXT,0,time,price)){ //--- Set text properties ObjectSetInteger(0,objNameDesc,OBJPROP_COLOR,clr); ObjectSetDouble(0,objNameDesc,OBJPROP_ANGLE,angle); if (direction > 0){ ObjectSetInteger(0,objNameDesc,OBJPROP_ANCHOR,ANCHOR_LEFT); ObjectSetString(0,objNameDesc,OBJPROP_TEXT," "+txt); } if (direction < 0){ ObjectSetInteger(0,objNameDesc,OBJPROP_ANCHOR,ANCHOR_BOTTOM); ObjectSetString(0,objNameDesc,OBJPROP_TEXT," "+txt); } } }
ここでは、チャート上に矢印と説明テキストを表示して売買シグナルを視覚的に表現するための「createSignalText」関数を定義します。この関数は、売買シグナルなどの重要なイベントをチャート上に明示することで、視認性を高める役割を果たします。まず、「StringConcatenate」関数を使用して、オブジェクト名を一意に生成します。この名前には、「Signal」という単語と、指定された時刻およびシグナルの価格が含まれます。これにより、チャート上の他のオブジェクトと名前が重複しないようにします。
次に、「ObjectCreate」関数を用いて、指定された時刻と価格の位置に矢印オブジェクトを作成します。作成が成功した場合、そのプロパティを設定します。「arrowcode」パラメータは表示する矢印の種類を決定し、「clr」パラメータは矢印の色を指定します。矢印の位置は、シグナルの方向に応じて、上向きの場合はANCHOR_TOP、下向きの場合はANCHOR_BOTTOMに設定され、矢印がシグナルのコンテキストに適切に配置されるようにします。
続いて、矢印に付随する説明テキストオブジェクトを作成します。テキストオブジェクトの名前は矢印名に「txt」という文字列を追加して一意にします。テキストオブジェクトも矢印と同じ時刻と価格の位置に配置されます。テキストの見た目や配置を整えるため、色は「clr」パラメータで設定し、「angle」パラメータで回転を指定します。上向きシグナルの場合、アンカーはANCHOR_LEFTに設定し、テキストの前にスペースを追加して位置を調整します。下向きシグナルの場合はアンカーをANCHOR_BOTTOMに設定し、同様にスペースを加えて見やすくします。
この関数を利用することで、シグナルに対応した矢印と注釈をチャート上に作成できます。
//--- FOR A BUY SIGNAL //--- Retrieve the time of the signal datetime textTime = iTime(_Symbol,_Period,1); //--- Retrieve the price of the signal double textPrice = iLow(_Symbol,_Period,1); //--- Create a visual signal on the chart for a buy createSignalText(textTime,textPrice,221,1,clrBlue,-90,"Buy Signal"); //... //--- FOR A SELL SIGNAL //--- Retrieve the time of the signal datetime textTime = iTime(_Symbol,_Period,1); //--- Retrieve the price of the signal double textPrice = iHigh(_Symbol,_Period,1); //--- Create a visual signal on the chart for a sell createSignalText(textTime,textPrice,222,-1,clrRed,90,"Sell Signal");
ここでは、チャート上に買い・売りシグナルを示す視覚的なマーカーを作成します。これらのマーカーは矢印と説明テキストで構成されており、チャートの見やすさを向上させ、判断を助ける役割を果たします。
買いシグナルの場合
- シグナルの時刻を取得する
「iTime」関数を使用して、現在の銘柄と時間枠(_Symbolと_Period)のチャート上で、1つ前に確定したバー(インデックス1)の日時を取得します。これにより、シグナルが確定したバーに対応していることが保証されます。
- シグナルの価格を取得する
同じバー(インデックス1)の最安値を「iLow」関数で取得します。。これがマーカーを置く位置となります。
- 視覚的なシグナルを作成する
取得した「textTime」と「textPrice」の値に加え、以下のパラメータを指定して「createSignalText」関数を呼び出します。
- 221:買いシグナルを表す特定の矢印コード
- 1:シグナルの方向(上昇を示す)
- clrBlue:矢印とテキストの色(ポジティブなシグナルを表す青色)
- -90:テキストの角度(適切な配置のため)
- 「Buy Signal」:矢印の近くに表示される説明テキスト。これにより、チャート上に買いシグナルが視覚的に表示されます。
売りシグナルの場合
- シグナルの時刻を取得する
買いシグナルと同様に、「iTime」関数を使用して、インデックス1のバー(1つ前に確定したバー)の日時を取得します。
- シグナルの価格を取得する
同じバーの最高値を「iHigh」関数で取得します。これが売りシグナルのマーカーを表示する位置となります。
- 視覚的なシグナルを作成する
以下のパラメータを指定して「createSignalText」関数を呼び出します。
- 222:売りシグナルを表す矢印コード
- -1:シグナルの方向(下降を示す)
- clrRed:矢印とテキストの色(ネガティブなシグナルを示す赤色)
- 90:テキストの角度(適切な配置のため)
- 「Sell Signal」:矢印の近くに表示される説明テキスト。これにより、チャート上に売りシグナルの明確なマーカーが追加されます。
プログラムを実行すると、次のような出力が得られます。
画像から分かるように、シグナルが確定すると、チャート上に矢印とそれに対応する注釈が表示され、シグナルの視認性が向上します。これにより、チャートにプロフェッショナルな印象が加わり、取引シグナルを視覚的に明確に把握しやすくなります。ここで、コードにトレーリングストップ機能を追加して、事前に定義された特定のレベルに達したときに利益を確定できるようになります。簡単にするために関数を使用します。
//+------------------------------------------------------------------+ //| Trailing stop function | //+------------------------------------------------------------------+ void applyTrailingStop(int slpoints, CTrade &trade_object,ulong magicno=0,int minProfitPts=0){ //--- Calculate the stop loss price for buy positions double buySl = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID) - slpoints*_Point,_Digits); //--- Calculate the stop loss price for sell positions double sellSl = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK) + slpoints*_Point,_Digits); //--- Loop through all positions in the account for (int i=PositionsTotal()-1; i>=0; i--){ //--- Get the ticket of the position ulong ticket = PositionGetTicket(i); //--- Ensure the ticket is valid if (ticket > 0){ //--- Select the position by ticket if (PositionSelectByTicket(ticket)){ //--- Check if the position matches the symbol and magic number (if provided) if (PositionGetSymbol(POSITION_SYMBOL) == _Symbol && (magicno == 0 || PositionGetInteger(POSITION_MAGIC) == magicno) ){ //--- Retrieve the open price and current stop loss of the position double positionOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); double positionSl = PositionGetDouble(POSITION_SL); //--- Handle trailing stop for buy positions if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){ //--- Calculate the minimum profit price for the trailing stop double minProfitPrice = NormalizeDouble((positionOpenPrice+minProfitPts*_Point),_Digits); //--- Apply trailing stop only if conditions are met if (buySl > minProfitPrice && buySl > positionOpenPrice && (positionSl == 0 || buySl > positionSl) ){ //--- Modify the position's stop loss trade_object.PositionModify(ticket,buySl,PositionGetDouble(POSITION_TP)); } } //--- Handle trailing stop for sell positions else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){ //--- Calculate the minimum profit price for the trailing stop double minProfitPrice = NormalizeDouble((positionOpenPrice-minProfitPts*_Point),_Digits); //--- Apply trailing stop only if conditions are met if (sellSl < minProfitPrice && sellSl < positionOpenPrice && (positionSl == 0 || sellSl < positionSl) ){ //--- Modify the position's stop loss trade_object.PositionModify(ticket,sellSl,PositionGetDouble(POSITION_TP)); } } } } } } }
ここでは、「applyTrailingStop」関数にトレーリングストップのメカニズムを実装し、アクティブな取引ポジションのストップロスレベルを動的に調整します。これにより、市場が有利に動いた場合に利益を確保しつつ、リスクを最小限に抑えることができます。この関数は次のロジックを使用して動作します。まず、買いポジションと売りポジションそれぞれのストップロスレベルを計算します。「SymbolInfoDouble」関数を使用して、SYMBOL_BID価格を取得し、指定された「slpoints」(ポイント単位のストップロス距離)を差し引き、「NormalizeDouble」関数を用いて小数点以下の桁数を正規化することで、「buySl」レベルを決定します。同様に、SYMBOL_ASK価格に「slpoints」を加算し、正規化して「sellSl」レベルを計算します。
次に、逆順のforループ「for (int i = PositionsTotal() - 1; i >= 0; i--)」を使って、口座内のすべてのアクティブなポジションを繰り返し処理します。各ポジションについて、「PositionGetTicket」関数でチケット番号を取得し、それが有効な場合には「PositionSelectByTicket」関数を使用して該当のポジションを選択します。ループ内では、そのポジションの銘柄が現在の「symbol」と一致するか、指定された「magicno」(マジックナンバー)と一致するかを確認します。「magicno」が0の場合は、マジックナンバーに関係なくすべてのポジションを対象とします。対象となるポジションに対しては、POSITION_PRICE_OPEN(建値)とPOSITION_SL(現在のストップロスレベル)を取得します。
買いポジションの場合、建値に「minProfitPts」(ポイント単位の最小利益)を加算して「minProfitPrice」を計算し、正規化します。「buySl」が以下のすべての条件を満たす場合に、トレーリングストップを適用します。
- 「minProfitPrice」を上回っている
- 建値よりも高い
- 現在のストップロスより高い、またはストップロスが未設定(「positionSl == 0」)
これらの条件をすべて満たす場合、CTradeオブジェクトの「PositionModify」メソッドを使用して、ストップロスを更新します。売りポジションの場合、建値から「minProfitPts」を差し引いて「minProfitPrice」を計算し、正規化します。同様に、「sellSl」が以下のすべての条件を満たす場合に、トレーリングストップを適用します。
- 「minProfitPrice」を下回っている
- 建値よりも低い
- 現在のストップロスより低い、またはストップロスが未設定(「positionSl == 0」)
条件をすべて満たした場合、同様に「PositionModify」メソッドを使用してストップロスを更新します。この関数は、ティックごとに呼び出すことで、すべてのオープンポジションに対してトレーリングストップロジックを適用することができます。
//--- Apply trailing stop if allowed in the input parameters if (inpisAllowTrailingStop){ applyTrailingStop(inpTrailPts,obj_Trade,inpMagicNo,inpMinTrailPts); }
ここで、トレーリングストップ関数を呼び出し、プログラムを実行すると、次の結果が得られます。
画像から、トレーリングストップの目的が正常に達成されたことが確認できます。次のステップとして、このデータをチャート上に視覚的に表示する必要があります。そのためには、メインのベースとラベルを備えたダッシュボードが必要です。ベース部分には、矩形ラベルを使用します。以下は、そのための関数の実装です。
//+------------------------------------------------------------------+ //| Create Rectangle label function | //+------------------------------------------------------------------+ bool createRecLabel(string objNAME,int xD,int yD,int xS,int yS, color clrBg,int widthBorder,color clrBorder = clrNONE, ENUM_BORDER_TYPE borderType = BORDER_FLAT,ENUM_LINE_STYLE borderStyle = STYLE_SOLID ){ //--- Reset the last error code ResetLastError(); //--- Attempt to create the rectangle label object if (!ObjectCreate(0,objNAME,OBJ_RECTANGLE_LABEL,0,0,0)){ //--- Log the error if creation fails Print(__FUNCTION__,": Failed to create the REC LABEL. Error Code = ",_LastError); return (false); } //--- Set rectangle label properties ObjectSetInteger(0, objNAME,OBJPROP_XDISTANCE, xD); ObjectSetInteger(0, objNAME,OBJPROP_YDISTANCE, yD); ObjectSetInteger(0, objNAME,OBJPROP_XSIZE, xS); ObjectSetInteger(0, objNAME,OBJPROP_YSIZE, yS); ObjectSetInteger(0, objNAME,OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, objNAME,OBJPROP_BGCOLOR, clrBg); ObjectSetInteger(0, objNAME,OBJPROP_BORDER_TYPE, borderType); ObjectSetInteger(0, objNAME,OBJPROP_STYLE, borderStyle); ObjectSetInteger(0, objNAME,OBJPROP_WIDTH, widthBorder); ObjectSetInteger(0, objNAME,OBJPROP_COLOR, clrBorder); ObjectSetInteger(0, objNAME,OBJPROP_BACK, false); ObjectSetInteger(0, objNAME,OBJPROP_STATE, false); ObjectSetInteger(0, objNAME,OBJPROP_SELECTABLE, false); ObjectSetInteger(0, objNAME,OBJPROP_SELECTED, false); //--- Redraw the chart to reflect changes ChartRedraw(0); return (true); }
「createRecLabel」関数では、チャート上にカスタマイズ可能な矩形ラベルを作成するために、一連の手順を実行します。まず、「ResetLastError」関数を使用して、以前のエラーコードをリセットします。次に、「ObjectCreate」関数を用いて矩形ラベルオブジェクトの作成を試みます。作成に失敗した場合は、エラーメッセージと失敗理由を表示し、falseを返します。作成に成功した場合は、「ObjectSetInteger」関数を使って、矩形ラベルのさまざまなプロパティを設定していきます。
これらのプロパティでは、矩形の位置、サイズ、背景色、枠線のスタイルなど、視覚的な要素を定義します。具体的には、xD、yD、xS、ySパラメータを用いて、OBJPROP_XDISTANCE、OBJPROP_YDISTANCE、OBJPROP_XSIZE、OBJPROP_YSIZEの各プロパティにそれぞれ値を設定します。さらに、OBJPROP_BGCOLORを使って背景色を、OBJPROP_BORDER_TYPEやOBJPROP_STYLEによって枠線の種類やスタイルを指定します。
最後に、ラベルの表示内容をチャートに反映させるために「ChartRedraw」関数を呼び出してチャートを更新します。すべてが正常に処理され、矩形ラベルが正しく作成されプロパティが設定された場合、この関数はtrueを返します。このようにして、与えられたパラメータに基づいて、チャート上にカスタマイズされた矩形ラベルを視覚的に表示できます。同様の方法で、通常のテキストラベルを作成する関数も実装できます。
//+------------------------------------------------------------------+ //| Create label function | //+------------------------------------------------------------------+ bool createLabel(string objNAME,int xD,int yD,string txt, color clrTxt = clrBlack,int fontSize = 12, string font = "Arial Rounded MT Bold" ){ //--- Reset the last error code ResetLastError(); //--- Attempt to create the label object if (!ObjectCreate(0,objNAME,OBJ_LABEL,0,0,0)){ //--- Log the error if creation fails Print(__FUNCTION__,": Failed to create the LABEL. Error Code = ",_LastError); return (false); } //--- Set label properties ObjectSetInteger(0, objNAME,OBJPROP_XDISTANCE, xD); ObjectSetInteger(0, objNAME,OBJPROP_YDISTANCE, yD); ObjectSetInteger(0, objNAME,OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetString(0, objNAME,OBJPROP_TEXT, txt); ObjectSetInteger(0, objNAME,OBJPROP_COLOR, clrTxt); ObjectSetString(0, objNAME,OBJPROP_FONT, font); ObjectSetInteger(0, objNAME,OBJPROP_FONTSIZE, fontSize); ObjectSetInteger(0, objNAME,OBJPROP_BACK, false); ObjectSetInteger(0, objNAME,OBJPROP_STATE, false); ObjectSetInteger(0, objNAME,OBJPROP_SELECTABLE, false); ObjectSetInteger(0, objNAME,OBJPROP_SELECTED, false); //--- Redraw the chart to reflect changes ChartRedraw(0); return (true); }
これらの関数を活用することで、必要に応じてダッシュボードを作成するための関数を実装できるようになりました。
//+------------------------------------------------------------------+ //| Create dashboard function | //+------------------------------------------------------------------+ void createDashboard(){ //--- }
ここでは、「createDashboard」という名前のvoid関数を作成し、ダッシュボードの作成ロジックをこの関数内に組み込みます。変更を効果的に追跡できるようにするために、まず「OnInit」イベントハンドラ内でこの関数を呼び出し、その後に関数本体を定義します。以下のように実装できます。
//--- createDashboard(); //---
関数を呼び出した後に、その本体を定義します。最初におこなうのはダッシュボード本体の定義であり、その名前はグローバル定数として定義する必要があります。
//+------------------------------------------------------------------+ //| Global constants for dashboard object names | //+------------------------------------------------------------------+ const string DASH_MAIN = "MAIN";
ここでは、定数文字列を定義します。constはプログラム全体を通じて変更されないことを意味します。次に、この定数を使ってラベルを作成します。以下のように実装します。
//+------------------------------------------------------------------+ //| Create dashboard function | //+------------------------------------------------------------------+ void createDashboard(){ //--- Create the main dashboard rectangle createRecLabel(DASH_MAIN,10,50+30,200,120,clrBlack,2,clrBlue,BORDER_FLAT,STYLE_SOLID); //--- }
「createDashboard」関数では、チャート上にビジュアルダッシュボードを作成するプロセスを開始します。これを実現するために、ダッシュボードのベースとなる四角形をチャート上に描画する役割を持つ「createRecLabel」関数を呼び出します。この関数には、四角形の外観や位置を定義するための特定のパラメータが渡されます。まず、この四角形の名前を「DASH_MAIN」と指定します。これにより、後からこのオブジェクトを識別できるようになります。次に、「xD」および「yD」パラメータを使って、チャート上の座標(10, 50+30)に四角形の左上隅を配置することで、位置を設定します。四角形の幅と高さはそれぞれ「xS」と「yS」パラメータで200ピクセルと120ピクセルに設定されており、必要に応じて後から調整可能です。
続いて、四角形の外観を定義します。背景色は「clrBlack」、境界線の色には青色(「clrBlue」)を指定します。境界線の太さは2ピクセル、スタイルは実線(STYLE_SOLID)、タイプはフラット(BORDER_FLAT)に設定されています。これらの設定により、四角形がはっきりと視認できるようになります。この四角形はダッシュボードの基礎要素として機能し、今後のステップでテキストやインタラクティブなコンポーネントなどを追加していくことができます。まずは、現時点のマイルストーンを実行して、その結果を確認してみましょう。
画像から、ダッシュボードのベースが予想通りであることがわかります。次に、ラベル関数を使用して同じ手順に従って、ダッシュボードの他の要素を作成します。残りのオブジェクトを次のように定義します。
//+------------------------------------------------------------------+ //| Global constants for dashboard object names | //+------------------------------------------------------------------+ const string DASH_MAIN = "MAIN"; const string DASH_HEAD = "HEAD"; const string DASH_ICON1 = "ICON 1"; const string DASH_ICON2 = "ICON 2"; const string DASH_NAME = "NAME"; const string DASH_OS = "OS"; const string DASH_COMPANY = "COMPANY"; const string DASH_PERIOD = "PERIOD"; const string DASH_POSITIONS = "POSITIONS"; const string DASH_PROFIT = "PROFIT";
ここでは、残りのオブジェクトを定義するだけです。ここでも、以下のようにラベル関数を使用してヘッダーラベルを作成します。
//+------------------------------------------------------------------+ //| Create dashboard function | //+------------------------------------------------------------------+ void createDashboard(){ //--- Create the main dashboard rectangle createRecLabel(DASH_MAIN,10,50+30,200,120+30,clrBlack,2,clrBlue,BORDER_FLAT,STYLE_SOLID); //--- Add icons and text labels to the dashboard createLabel(DASH_ICON1,13,53+30,CharToString(40),clrRed,17,"Wingdings"); createLabel(DASH_ICON2,180,53+30,"@",clrWhite,17,"Webdings"); createLabel(DASH_HEAD,65,53+30,"Dashboard",clrWhite,14,"Impact"); }
ここでは、「createLabel」関数を使用してアイコンと見出しを追加し、ダッシュボードをさらに強化します。この関数は複数回呼び出され、テキストベースの要素をチャート上の特定位置に配置することで、視覚的に魅力的で情報豊富なインターフェースを構築できます。まず、「DASH_ICON1」というラベル名でアイコンを作成し、チャートの相対座標(13, 53+30)に配置します。このアイコンは文字コード40を「CharToString(40)」関数で文字列に変換して表現されます。赤色(clrRed)でフォントサイズ17、フォントスタイルはWingdingsに設定されており、文字がグラフィカルなシンボルとして表示されます。
続いて、「DASH_ICON2」という別のアイコンを座標(180, 53+30)に配置します。このアイコンは「@」文字を使用し、白色(clrWhite)、フォントサイズ17で表示されます。フォントスタイルはWebdingsに設定されており、「@」が装飾的かつ様式的なシンボルとして描画されます。以下にその表現を示します。
最後に、「DASH_HEAD」というラベル名のテキスト見出しを座標(65, 53+30)に追加します。この見出しは「Dashboard」というテキストを表示し、白色(clrWhite)、フォントサイズ14で描画されます。フォントスタイルは「Impact」に設定されており、太字で特徴的な外観を持たせています。この後、残りのラベルを定義していくことができます。
createLabel(DASH_NAME,20,90+30,"EA Name: Crossover RSI Suite",clrWhite,10,"Calibri"); createLabel(DASH_COMPANY,20,90+30+15,"LTD: "+AccountInfoString(ACCOUNT_COMPANY),clrWhite,10,"Calibri"); createLabel(DASH_OS,20,90+30+15+15,"OS: "+TerminalInfoString(TERMINAL_OS_VERSION),clrWhite,10,"Calibri"); createLabel(DASH_PERIOD,20,90+30+15+15+15,"Period: "+EnumToString(Period()),clrWhite,10,"Calibri"); createLabel(DASH_POSITIONS,20,90+30+15+15+15+30,"Positions: "+IntegerToString(PositionsTotal()),clrWhite,10,"Calibri"); createLabel(DASH_PROFIT,20,90+30+15+15+15+30+15,"Profit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY),clrWhite,10,"Calibri");
ここでは、「createLabel」関数を使用して、ダッシュボードに重要な情報ラベルを追加します。まず、座標(20, 90+30)に「DASH_NAME」ラベルを作成します。このラベルは「EA Name: Crossover RSI Suite」というテキストを表示し、白色(clrWhite)、フォントサイズ10、フォントスタイルは「Calibri」に設定されています。このラベルはEAの名前を示し、ユーザーに明確な識別情報を提供します。
次に、座標(20, 90+30+15)に「DASH_COMPANY」ラベルを追加します。このラベルは「LTD: 」というテキストの後に、「AccountInfoString」関数にACCOUNT_COMPANYパラメータを渡して取得されたアカウントの会社情報を表示します。スタイルは白色、フォントサイズ10、フォントは「Calibri」です。続いて、(20, 90+30+15+15)に「DASH_OS」ラベルを配置します。このラベルは「OS: 」というテキストと、TerminalInfoString関数にTERMINAL_OS_VERSIONを渡して得られたOSのバージョン情報を組み合わせて表示します。これにより、ユーザーは使用中のターミナルのオペレーティングシステムを確認できます。このラベルも白色、フォントサイズ10、「Calibri」フォントで統一されています。
その後、(20, 90+30+15+15+15)に「DASH_PERIOD」ラベルを追加します。このラベルには「Period: 」というテキストと、現在の時間枠を「EnumToString」関数で文字列化した結果を表示します。テキストは白色、フォントサイズは小さめで「Calibri」フォントを使用し、ダッシュボード全体のデザインとの一貫性を保ちます。さらに、(20, 90+30+15+15+15+30)に「DASH_POSITIONS」ラベルを配置します。このラベルは「Positions: 」というテキストに続けて、現在口座で保有しているポジションの合計数を表示します。この情報はアクティブな取引の状況を把握するために非常に重要です。
最後に、(20, 90+30+15+15+15+30+15)に「DASH_PROFIT」ラベルを追加します。このラベルは「Profit: 」というテキストに続けて、口座の現在の利益を小数点第2位まで表示し、その後にAccountInfoString関数で取得された口座通貨を表示します。
また、プログラムが終了または削除された際には、作成したダッシュボードを削除する処理が必要です。そのため、ダッシュボードを削除するための関数も用意しておく必要があります。
//+------------------------------------------------------------------+ //| Delete dashboard function | //+------------------------------------------------------------------+ void deleteDashboard(){ //--- Delete all objects related to the dashboard ObjectDelete(0,DASH_MAIN); ObjectDelete(0,DASH_ICON1); ObjectDelete(0,DASH_ICON2); ObjectDelete(0,DASH_HEAD); ObjectDelete(0,DASH_NAME); ObjectDelete(0,DASH_COMPANY); ObjectDelete(0,DASH_OS); ObjectDelete(0,DASH_PERIOD); ObjectDelete(0,DASH_POSITIONS); ObjectDelete(0,DASH_PROFIT); //--- Redraw the chart to reflect changes ChartRedraw(); }
ここでは、void関数「deleteDashboard」をを作成します。この関数内では、「ObjectDelete」関数を使用して、使用してすべてのオブジェクト名を指定して削除し、最後に「ChartRedraw」関数を呼び出して、変更をチャートに反映させます。このdeleteDashboard関数は、初期化解除関数内で呼び出されます。また、保有ポジションが変化するたびに、ポジション数や利益などの情報を正確に表示するために、ダッシュボードを随時更新する必要があります。以下に、そのために採用しているロジックを示します。
if (PositionsTotal() > 0){ ObjectSetString(0,DASH_POSITIONS,OBJPROP_TEXT,"Positions: "+IntegerToString(PositionsTotal())); ObjectSetString(0,DASH_PROFIT,OBJPROP_TEXT,"Profit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY)); }
ここでは、ポジションが0より大きい場合はポジションがあり、そのプロパティを更新できることを確認します。結果は次のとおりです。
可視化からわかるように、ポジションがクローズされた後にダッシュボードが更新されないことが確認できます。したがって、ポジションがクローズされたタイミングを追跡し、ポジションが存在しない場合には、ダッシュボードの値をデフォルトに戻す必要があります。これを実現するには、OnTradeTransactionイベントハンドラが必要になります。
//+------------------------------------------------------------------+ //| OnTradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction( const MqlTradeTransaction& trans, // trade transaction structure const MqlTradeRequest& request, // request structure const MqlTradeResult& result // response structure ){ if (trans.type == TRADE_TRANSACTION_DEAL_ADD){ Print("A deal was added. Make updates."); if (PositionsTotal() <= 0){ ObjectSetString(0,DASH_POSITIONS,OBJPROP_TEXT,"Positions: "+IntegerToString(PositionsTotal())); ObjectSetString(0,DASH_PROFIT,OBJPROP_TEXT,"Profit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY)); } } }
ここでは、取引関連のトランザクションが発生するたびに呼び出されるOnTradeTransaction関数を設定します。この関数は、取引イベントを処理し、特定のアクションに応じてダッシュボード上の関連情報を更新します。まず、MqlTradeTransaction型の「trans」パラメータで提供されるトレードトランザクションの型がTRADE_TRANSACTION_DEAL_ADDと等しいかどうかを確認します。この条件は、新しい取引が口座に追加されたかどうかを判定するためのものです。このようなトランザクションが検出された場合、デバッグや情報確認の目的で、「A deal was added. Make updates.」というメッセージをログに出力します。
次に、PositionsTotal関数を使用して取得した現在のポジションの合計数が0以下であるかを確認します。これにより、口座にアクティブなポジションが残っていない場合にのみ、ダッシュボードの更新がおこなわれるようにします。この条件が満たされた場合、ObjectSetString関数を使用して、ダッシュボード上の2つのラベルを更新します。以下はその結果です。
画像から分かるように、各取引が実行されるたびに更新が反映されており、目的は達成されています。あとは、プログラムをバックテストしてパフォーマンスを分析するのみです。これについては次のセクションで扱います。
バックテスト
徹底的なバックテストの結果、次の結果が得られました。
バックテストグラフ
バックテストレポート
こちらは、2024年の1年間にわたるストラテジーのバックテスト全体を紹介する動画形式の資料です。
結論
結論として、テクニカル指標、自動取引管理、インタラクティブなダッシュボードを統合した、堅牢なMQL5EAを開発する方法を紹介しました。移動平均のクロスオーバー、相対力指数(RSI)、トレーリングストップなどのツールに、動的な取引更新やトレーリングストップ、ユーザーフレンドリーなインターフェイスといった機能を組み合わせることで、シグナルを生成し、取引を管理し、効果的な意思決定のためにリアルタイムでの洞察を提供するEAを構築しました。
免責条項:本記事は教育目的のみを意図したものです。取引には大きな財務リスクが伴い、市場の状況は予測不可能な場合があります。本記事で紹介した戦略は、構造化されたフレームワークを提供するものですが、過去の実績が将来の成果を保証するものではありません。実際に運用を開始する前に、徹底したテストと適切なリスク管理が不可欠です。
これらの概念を適用することで、より適応性の高い取引システムを構築し、アルゴリズム取引戦略を強化できます。皆さんのコーディングの成功と、取引での成果を心より願っています。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/17040





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
こんにちは。ビデオには、コンパイル、テスト期間、使用した入力パラメータなど、すべてが示されています。