English Русский 中文 Español Deutsch Português
preview
MQL5-Telegram統合エキスパートアドバイザーの作成(第2回):MQL5からTelegramへのシグナル送信

MQL5-Telegram統合エキスパートアドバイザーの作成(第2回):MQL5からTelegramへのシグナル送信

MetaTrader 5トレーディングシステム | 11 10月 2024, 16:17
285 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

MQL5用のTelegram統合型エキスパートアドバイザー(EA)の開発に関する本連載の第1部では、MQL5とTelegramを連携させるために必要な基本的なステップについて説明しました。最初のステップは、アプリケーションの設定でした。その後、コーディング作業に進みましたが、この順序には重要な理由があり、それについては次の段落で詳しく説明します。その結果、メッセージを受信できるボットと、メッセージを送信できるプログラムを完成させました。また、ボットを通じてアプリケーションにメッセージを送信する方法を示す、簡単なMQL5プログラムも作成しました。

第1回で基礎が整ったので、今回はMQL5を用いてTelegramに売買シグナルを送信します。新たに強化されたEAは、単に事前設定された条件に基づいて取引を開始・終了するだけでなく、Telegramのグループチャットに取引成立のシグナルを送信するという機能も備えています。取引シグナル自体も、Telegramで伝達する情報ができるだけ明確かつ簡潔になるように調整しました。この新しいバージョンのChatty Traderは、以前のものよりもTelegram上でスムーズにグループと会話し、取引成立や決済のシグナルをほぼリアルタイムで受信できるように改善されています。

今回は、よく知られた移動平均クロスオーバーシステムを基にシグナルを生成し、それをTelegramに中継します。振り返ると、第1回では1つのメッセージしか送信できず、追加のセグメントを含めるとエラーが発生するという制限がありました。そのため、一度に送信できるメッセージは1つだけであり、もし余分なセグメントがあれば、別の個別のメッセージで中継しなければなりませんでした。例えば、「買いシグナルが発生しました」と「買い注文を出してください」というメッセージを送る場合、1つの長いメッセージか、2つの短いメッセージに分ける必要があったのです。今回は、複数のテキストセグメントを1つのメッセージにまとめる機能を追加し、改善をおこないます。このプロセス全体については、以下のサブトピックで説明します。

  1. 戦略の概要
  2. MQL5での実装
  3. 統合のテスト
  4. 結論

最終的には、生成されたシグナルや発注された注文などの取引情報を取引端末から指定されたTelegramチャットに送信するEAを作成します。始めましょう。


戦略の概要

移動平均クロスオーバーは、最も広く使用されているテクニカル分析ツールの一つです。移動平均のクロスオーバーを利用して、潜在的な売買機会を見極めるための、最も単純明快な方法について説明します。これは、他のツールや指標を加えることなく、クロスオーバー自体のシグナルの性質に基づいています。簡単にするために、ここでは期間の異なる2つの移動平均(短期移動平均と長期移動平均)のみを考えます。

移動平均のクロスオーバーの機能と、クロスオーバーがどのように売買シグナルを発生させるかについて解説します。移動平均線は、価格データを取得し、それを滑らかにし、実際の価格チャートよりもトレンドの識別にはるかに優れている一種の流れるような線を作成します。一般的に、平均はギザギザの線よりも常に合理的で、たどりやすいからです。異なる期間の移動平均線を2つ追加すると、ある時点で交差するため、「クロスオーバー」と呼ばれます。 

MQL5を使って移動平均クロスオーバーシグナルを実践するには、まず、取引戦略に最も合致する平均の短期と長期の期間を決定します。このため、長期的なトレンドには50年、200年、短期的なトレンドには10年、20年といった標準的な期間を用います。移動平均を計算した後、新しいティックまたはバーごとにクロスオーバーイベントの値を比較し、検出されたクロスオーバーシグナルを「買い」または「売り」のバイナリイベントに変換し、EAが対応できるようにします。私たちが何を言いたいのかを簡単に理解するために、2つの事例を可視化してみましょう。

アップワードクロスオーバー

アップワードクロスオーバー

ダウンワードクロスオーバー

ダウンダウンクロスオーバー

これらの生成されたシグナルは、現在のMQL5-Telegramメッセージングフレームワークと組み合わされます。これを実現するために、第1回のコードはシグナル検出とフォーマットを含むように適応されます。クロスオーバーが確認されると、資産名、クロスオーバーの方向(買い/売り)、シグナル時刻を含むメッセージが作成されます。このメッセージを指定されたTelegramチャットにタイムリーに配信することで、取引グループが潜在的な取引機会について常に情報を得られるようになります。何よりも、クロスオーバーが発生した直後にメッセージを確実に受信できるということは、問題のシグナルに基づいて取引を開始したり、市場ポジションを開いてポジションの詳細を中継したりする機会が得られることを意味します。


MQL5での実装

まず、メッセージをセグメント化し、全体として送信できるようにします。第1回では、改行などの特殊文字を含む複雑なメッセージを送信するとエラーが発生し、構造を持たない単一のメッセージとしてしか送信できませんでした。例えば、次の初期化イベント、口座資本、利用可能な余剰証拠金を取得するコードスニペットがあった場合

   double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   string msg = "🚀 EA INITIALIZED ON CHART " + _Symbol + " 🚀"
                +"📊 Account Status 📊; Equity: $"
                +DoubleToString(accountEquity,2)
                +"; Free Margin: $"
                +DoubleToString(accountFreeMargin,2);

これを全体として送ると、こうなります。

ロングメッセージ

メッセージを送ることはできても、その構造には魅力がないことがわかります。初期化文を最初の行に、次に口座状況を2行目に、エクイティを次の行に、そして余剰証拠金情報を最後の行に書くべきです。そのためには、次のように改行文字「\n」を考慮する必要があります。

   double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   string msg = "🚀 EA INITIALIZED ON CHART " + _Symbol + " 🚀"
                +"\n📊 Account Status 📊"
                +"\nEquity: $"
                +DoubleToString(accountEquity,2)
                +"\nFree Margin: $"
                +DoubleToString(accountFreeMargin,2);

しかし、プログラムを実行すると、操作ログに図のようなエラーメッセージが表示され、メッセージがTelegramチャットに送信されません。

改行エラー

メッセージが正常に送信されるようにするには、エンコーディングする必要があります。私たちの統合では、特殊文字を適切に処理するためにメッセージのエンコーディングが必要です。例えば、メッセージにスペースや記号(&や? など)のようなものが含まれている場合、統合時の注意が不十分なために、Telegram Application Programming Interface (API)によって誤読される可能性があります。これは真剣に受け止めています。冗談ではありません。文字エンコーディングの他の使い方、例えば、コンピューターである種の文書を開くときに、このような使い方をするのを見たことがあります。

文書エンコーディング

エンコーディングは、API が何を送信しようとしているのか理解できず、目的の動作が実行できないといった、これまで遭遇してきたような種類の問題を回避するための鍵です。

例えば、特殊な文字を含むAPIに送られたメッセージは、Uniform Resource Locator (URL)構造(URLがコンピュータによって「見られる」方法)に干渉し、解釈のエラーを引き起こす可能性があります。APIは特殊文字を実際のメッセージの一部としてではなく、命令やコードの他の部分として解釈するかもしれません。つまり、プログラムからメッセージを送信するときと、もう一方のエンコーディングが、メッセージの見えない部分を安全に見ることができるようにするという主要な機能を果たしていないのです。また、エンコードスキームを使うということは、受信側(この場合はTelegram API)と互換性のあるフォーマットでメッセージがあるということです。結局のところ、この話には複数の異なるシステムが関与しており、それぞれが、どのようにデータを渡してほしいかについて特定の要件を持っています。したがって、まず最初にすることは、メッセージをエンコードする関数を作ることです。

// FUNCTION TO ENCODE A STRING FOR USE IN URL
string UrlEncode(const string text) {
    string encodedText = ""; // Initialize the encoded text as an empty string
    int textLength = StringLen(text); // Get the length of the input text

   ...

}

ここでは、まず、提供されたテキストをURLエンコード形式に変換するように設計された、単一のパラメータまたは引数(文字列型テキスト)を受け取る「UrlEncode」という文字列データ型関数を作成します。次に、空の文字列encodedTextを初期化します。この文字列は、入力テキストを処理する際に、URLエンコードされた結果を構築するために使用されます。次に、StringLen関数を使って入力文字列の長さを決定し、この長さを整数変数textLengthに格納します。このステップで、処理に必要な文字数を知ることができるので、非常に重要です。長さを保存することで、文字列の各文字をループで効率的に繰り返し処理することができ、すべての文字がURLエンコーディング規則に従って正しくエンコードされていることを確認できます。反復処理にはループを使う必要があります。

    // Loop through each character in the input string
    for (int i = 0; i < textLength; i++) {
        ushort character = StringGetCharacter(text, i); // Get the character at the current position
   
        ...

    }

ここでは、入力メッセージまたはテキストに含まれるすべての文字を、インデックス0以降の最初の文字から順に反復処理するために、forループを開始します。StringGetCharacter関数を使用して、選択した銘柄の値を取得します。この関数は通常、文字列の指定された位置にある銘柄の値を返します。位置はインデックス「i」で定義されます。その文字をcharacterという名前のushhort変数に格納します。

        // Check if the character is alphanumeric or one of the unreserved characters
        if ((character >= 48 && character <= 57) ||  // Check if character is a digit (0-9)
            (character >= 65 && character <= 90) ||  // Check if character is an uppercase letter (A-Z)
            (character >= 97 && character <= 122) || // Check if character is a lowercase letter (a-z)
            character == '!' || character == '\'' || character == '(' ||
            character == ')' || character == '*' || character == '-' ||
            character == '.' || character == '_' || character == '~') {

            // Append the character to the encoded string without encoding
            encodedText += ShortToString(character);
        }

ここでは、指定された文字が英数字か、URLでよく使われる予約なしの文字かどうかをチェックします。目的は、その文字をエンコードする必要があるのか、エンコードされた文字列に直接追加できるのかを判断することです。まず、ASCII値が48から57の間にあるかどうかを確認することによって、その文字が数字であるかどうかをチェックします。次に、ASCII 値が65から90の間にあるかどうかで、その文字が大文字かどうかをチェックします。同様に、ASCII値が97から122の間にあるかどうかを確認することで、その文字が小文字かどうかをチェックします。これらの値は「ASCII表」から確認できます。

数字:48から57

DIGITS

大文字:65から90

大文字

小文字:97から122

小文字

これらの英数字に加え、URLで使用される特定の未予約文字もチェックします。これらには「!」、「'」、「(」、「)」、「*」、「-」、「.」、「_」、「~」が含まれます。文字がこれらの基準のいずれかに一致する場合、その文字は英数字か、または予約されていない文字のいずれかであることを意味します。

文字がこれらの条件のいずれかを満たす場合、その文字をエンコードせずにencodedText文字列に追加します。これは、ShortToString関数を使用して文字を文字列表現に変換することで達成され、文字が元の形式でエンコードされた文字列に追加されることを保証します。これらの条件のいずれにも当てはまらない場合は、スペース文字のチェックに進みます。

        // Check if the character is a space
        else if (character == ' ') {
            // Encode space as '+'
            encodedText += ShortToString('+');
        }

ここでは、else if文を使って、スペース文字と比較することによって、その文字がスペースかどうかをチェックしています。もしその文字が本当にスペースなら、URLに適した方法でエンコードする必要があります。コンピュータ文書の場合に見られたような、スペースに対する典型的なパーセントエンコーディング(%20)を使用する代わりに、スペースをプラス記号「+」としてエンコードすることにしました。これは、特にクエリ コンポーネント内のURL内のスペースを表すためのもう1つの一般的な方法です。そこで、ShortToString 関数を使ってプラス記号「+」を文字列表現に変換し、encodedText文字列に追加します。

ここまででコード化されていない文字があるとすれば、それは絵文字のような複雑な文字であるため、頭を悩ませることになります。したがって、英数字、予約なし、スペース以外のすべての文字を、Unicode Transformation Format-8 (UTF-8)を使用してエンコードすることで、先にチェックしたカテゴリに分類されない文字がURLに含まれるように安全にエンコードされるように処理する必要があります。

        // For all other characters, encode them using UTF-8
        else {
            uchar utf8Bytes[]; // Array to hold the UTF-8 bytes
            int utf8Length = ShortToUtf8(character, utf8Bytes); // Convert the character to UTF-8
            for (int j = 0; j < utf8Length; j++) {
                // Convert each byte to its hexadecimal representation prefixed with '%'
                encodedText += StringFormat("%%%02X", utf8Bytes[j]);
            }
        }

まず、文字のUnicode Transformation Format-8 (UTF-8)バイト表現を保持する配列utf8Bytesを宣言します。次に、引数としてcharacterとutf8Bytes配列を渡して、ShortToUtf8関数を呼び出します。この関数についてはすぐに説明しますが、今は、この関数は文字をUTF-8表現に変換し、変換に使用されたバイト数を返し、そのバイト数をutf8Bytes配列に格納する、ということだけ知っておいてください。

次に、forループを使ってutf8Bytes配列の各バイトを繰り返し処理します。各バイトについて、URLの文字をパーセントエンコードする標準的な方法である「%」文字を前置した16進表現に変換します。StringFormat関数を使用して、各バイトを「%」をを前置した2桁の16進数としてフォーマットします。最後に、このエンコードされた表現をencodedText文字列に追加します。最終的には結果を返すだけです。

    return encodedText; // Return the URL-encoded string

関数の全コードスニペットは以下の通りです。

// FUNCTION TO ENCODE A STRING FOR USE IN URL
string UrlEncode(const string text) {
    string encodedText = ""; // Initialize the encoded text as an empty string
    int textLength = StringLen(text); // Get the length of the input text

    // Loop through each character in the input string
    for (int i = 0; i < textLength; i++) {
        ushort character = StringGetCharacter(text, i); // Get the character at the current position

        // Check if the character is alphanumeric or one of the unreserved characters
        if ((character >= 48 && character <= 57) ||  // Check if character is a digit (0-9)
            (character >= 65 && character <= 90) ||  // Check if character is an uppercase letter (A-Z)
            (character >= 97 && character <= 122) || // Check if character is a lowercase letter (a-z)
            character == '!' || character == '\'' || character == '(' ||
            character == ')' || character == '*' || character == '-' ||
            character == '.' || character == '_' || character == '~') {

            // Append the character to the encoded string without encoding
            encodedText += ShortToString(character);
        }
        // Check if the character is a space
        else if (character == ' ') {
            // Encode space as '+'
            encodedText += ShortToString('+');
        }
        // For all other characters, encode them using UTF-8
        else {
            uchar utf8Bytes[]; // Array to hold the UTF-8 bytes
            int utf8Length = ShortToUtf8(character, utf8Bytes); // Convert the character to UTF-8
            for (int j = 0; j < utf8Length; j++) {
                // Convert each byte to its hexadecimal representation prefixed with '%'
                encodedText += StringFormat("%%%02X", utf8Bytes[j]);
            }
        }
    }
    return encodedText; // Return the URL-encoded string
}

文字をUTF-8表現に変換する関数を見てみましょう。

//+-----------------------------------------------------------------------+
//| Function to convert a ushort character to its UTF-8 representation    |
//+-----------------------------------------------------------------------+
int ShortToUtf8(const ushort character, uchar &utf8Output[]) {

   ...

}

この関数は整数データ型で、2つの入力パラメータ、文字値と出力配列を受け取ります。 

まず、半角文字を変換します。

    // Handle single byte characters (0x00 to 0x7F)
    if (character < 0x80) {
        ArrayResize(utf8Output, 1); // Resize the array to hold one byte
        utf8Output[0] = (uchar)character; // Store the character in the array
        return 1; // Return the length of the UTF-8 representation
    }

0x00から0x7Fの範囲の値を持つ1バイト文字の変換は、UTF-8では1バイトで直接表現されるため、簡単です。まず、その文字が0x80より小さいかどうかをテストします。もしそうであれば、ArrayResize関数を使ってutf8Output配列のサイズを1バイトだけ変更します。これにより、出力されるUTF-8表現に正しいサイズを持たせることができます。その文字をucharにキャストすることで、配列の最初の要素にその文字を入れます。これは型キャストと呼ばれます。これは、文字の値を配列にコピーするのと同じことです。1を返し、UTF-8表現の長さが1バイトであることを示します。この処理は、オペレーティングシステムに関係なく、あらゆる1バイト文字をUTF-8形式に効率的に変換します。

その代表的なものは以下の通りです。

0x00、UTF-8:

0x00 UTF-8

0x7F、UTF-8:

0x7F UTF-8

10進数で表すと0~127であることがわかります。これらの文字がユニコードの初期文字と同じであることにお気づきでしょう。おそらく、これが何なのか不思議に思っていることでしょう。少し立ち止まって、もっと深く見てみましょう。16進数表記では、0x80と0x7Fは特定の値を表し、理解を深めるために10進数に変換することができます。16進数の0x80は10進数の128に相当します。これは、16進数が16を基数とする数値システムであり、各桁が16の累乗を表すためです。0x80では、8は16^1の8倍(つまり128)を表し、0は16^0の0倍(つまり0)を表すので、合計は128となります。

一方、0x7Fは10進数で127に相当します。16進数では、7Fは16^1の7倍と16^0の15倍の和です。計算すると、7*16 (112) + F (15) となり、127となります。以下のA~Fの表現を参照してください。16進数Fの10進数は15に等しいです。

HEX、A-F

したがって、0x80は10進数で128であり、0x7Fは10進数で127です。つまり、0x80は0x7Fより1つ多いだけで、UTF-8エンコーディングのシングルバイト表現がマルチバイト表現に変わる境界となります。

進行形式やすべての意味について疑問が残らないように、詳しく説明したかったのですが、これでお分かりだと思います。次に2バイト文字を見てみましょう。

    // Handle two-byte characters (0x80 to 0x7FF)
    if (character < 0x800) {
        ArrayResize(utf8Output, 2); // Resize the array to hold two bytes
        utf8Output[0] = (uchar)((character >> 6) | 0xC0); // Store the first byte
        utf8Output[1] = (uchar)((character & 0x3F) | 0x80); // Store the second byte
        return 2; // Return the length of the UTF-8 representation
    }


ここでは、UTF-8表現で2バイトを必要とする文字、つまり0x80と0x7FFの間の値を持つ文字の変換をおこないます。これをおこなうには、まず問題の文字が0x800(10進数で2048)より小さいかどうかをテストし、それが本当にこの範囲内にあることを保証します。この条件が満たされた場合、utf8Output配列を2バイトにサイズ変更します(UTF-8で文字を表現するのに2バイトかかるため)。次に、実際のUTF-8表現を計算します。

最初のバイトは、この文字を6ビット右にシフトし、論理和演算で0xC0と結合することで得られます。この計算では、最初のバイトの最上位ビットを、2バイト文字のUTF-8接頭辞に設定します。2バイト目は、0x3Fで文字をマスクして下位6ビットを取得し、これを0x80と組み合わせることで計算されます。この操作により、2バイト目が正しいUTF-8接頭辞を持つことが保証されます。

最後に、この2バイトをutf8Output配列に入れ、呼び出し元に2を報告します。これは、その文字がUTF-8表現で2バイト必要であることを示します。これは、1バイト文字に比べて2倍のビット数を使用する文字にとって必要かつ正しいエンコーディングです。これで3バイト文字が得られます。

    // Handle three-byte characters (0x800 to 0xFFFF)
    if (character < 0xFFFF) {

        ...

    }

もうお分かりでしょう。ここで、16進数0xFFFFは10進数で65,535に変換されます。16進数の各桁が16の累乗を表していることは認識しています。0xFFFFの場合、各桁はFであり、10進数では15です。その10進数の値を計算するために、各桁の貢献をその位置に基づいて評価します。最も高い位の値から始めます。これは、(15 * 16^3)つまり(15 * 4096 = 61,440)となります。次に、(15 * 16^2)を計算すると(15 * 256 = 3,840)となります。(15 * 16^1)は(15 * 16 = 240)です。最後に、(15 * 16^0)は(15 * 1 = 15)に等しいです。これらの結果を足すと、61,440+3,840+240+15で、合計65,535となります。したがって、0xFFFFは10進数で65,535となります。このように考えると、3バイト文字は3つ存在することになります。最初の例を見てみましょう。

        if (character >= 0xD800 && character <= 0xDFFF) { // Ill-formed characters
            ArrayResize(utf8Output, 1); // Resize the array to hold one byte
            utf8Output[0] = ' '; // Replace with a space character
            return 1; // Return the length of the UTF-8 representation
        }

ここでは、Unicodeの範囲0xD800から0xDFFFに入る文字を扱います。これはサロゲートハーフとして知られており、単独の文字としては無効です。まず、その文字がこの範囲内にあるかどうかをチェックします。このような不正な形式の文字に遭遇した場合、まずutf8Output配列のサイズを1バイトだけ保持するように変更し、出力配列が1バイトだけ格納できるようにします。

次に、utf8Output配列の最初の要素をスペースに設定することで、無効な文字をスペース文字に置き換えます。この選択肢は、無効な入力を潔く処理するためのプレースホルダーです。最後に、この不正な文字のUTF-8表現が1バイト長であることを示す1を返します。次に、絵文字をチェックします。つまり、Unicodeの0xE000から0xF8FFの範囲内にある文字を扱うということです。これらの文字には、絵文字やその他の拡張記号が含まれます。

        else if (character >= 0xE000 && character <= 0xF8FF) { // Emoji characters
            int extendedCharacter = 0x10000 | character; // Extend the character to four bytes
            ArrayResize(utf8Output, 4); // Resize the array to hold four bytes
            utf8Output[0] = (uchar)(0xF0 | (extendedCharacter >> 18)); // Store the first byte
            utf8Output[1] = (uchar)(0x80 | ((extendedCharacter >> 12) & 0x3F)); // Store the second byte
            utf8Output[2] = (uchar)(0x80 | ((extendedCharacter >> 6) & 0x3F)); // Store the third byte
            utf8Output[3] = (uchar)(0x80 | (extendedCharacter & 0x3F)); // Store the fourth byte
            return 4; // Return the length of the UTF-8 representation
        }

まず、その文字が絵文字の範囲内にあるかどうかを判断します。この範囲にある文字はUTF-8で4バイト表現が必要なので、まず0x10000とビットORを実行して文字値を拡張します。このステップによって、補助プレーンの文字を正しく処理することができます。

続いて、utf8Output配列のサイズを4バイトに変更します。これにより、UTF-8エンコーディング全体を配列に格納するのに十分なスペースが確保されます。つまり、UTF-8表現の計算は、4つの部分(4バイト)を導き出し、組み合わせることに基づいています。最初のバイトについては、extendedCharacterを18ビット右にシフトします。次に、この値と0xF0を論理的に組み合わせ(ビット毎OR演算、または|を使用)、最初のバイトの適切な 上位 ビットを得ます。2バイト目は、extendedCharacterを12ビット右にシフトし、同様に次の部分を取得します。

同様に、拡張文字6ビットを右シフトし、次の6ビットをマスクすることで3バイト目を計算します。これを0x80と組み合わせ、3バイト目の最初の部分を得ます。2番目の部分を得るには、拡張文字の最後の6ビットが得るために拡張文字を0x3Fでマスクし、それを0x80と組み合わせます。これらの 2バイトを計算してutf8Output配列に格納した後、文字がUTF-8で4バイトを占めることを示す4を返します。例えば、1F4B0という絵文字があります。これはマネーバッグの絵文字です。

マネー絵文字

その10進数表現を計算するために、まず16進数を10進数に変換します。16^4位の数字の1は、1*65,536=65,536に寄与します。10進数で15であるFの16^3の位は、15*4,096=61,440となります。16^2の位の数字の4は、4*256=1,024に寄与します。10進数で11となるBの16^1の位は、11*16=176となります。最後に、16^0位の0は0*1=0となります。

これらを足すと、65,536+61,440+1,024+176+0=128,176となります。したがって、0x1F4B0は10進数で128,176に変換されます。提供された画像で確認できます。

最後に、以前に扱った特定の範囲から外れて、3バイトのUTF-8表現が必要な文字に対処します。

        else {
            ArrayResize(utf8Output, 3); // Resize the array to hold three bytes
            utf8Output[0] = (uchar)((character >> 12) | 0xE0); // Store the first byte
            utf8Output[1] = (uchar)(((character >> 6) & 0x3F) | 0x80); // Store the second byte
            utf8Output[2] = (uchar)((character & 0x3F) | 0x80); // Store the third byte
            return 3; // Return the length of the UTF-8 representation
        }

まずutf8Output配列のサイズを変更し、必要な3バイトを格納できるようにします。各バイトのサイズは8なので、3バイトを保持するには24ビット分のスペースが必要です。次に、UTF-8エンコーディングの3バイトをバイトごとに計算します。最初のバイトは文字の先頭部分から決定されます。2バイト目を計算するには、文字を6ビット右にシフトし、その結果をマスクして次の6ビットを取得し、これを0x80と組み合わせて継続ビットを設定します。3バイト目の取得も、シフトしないことを除けば、概念的には同じです。代わりに、最後の6ビットを取得するためにマスクし、0x80と組み合わせます。utf8Output配列に格納されている3バイトを決定した後、3バイトにまたがる表現であることを示す3を返します。

最後に、文字が無効であったり、正しくエンコードできない場合は、ユニコードの置換文字U+FFFDで置き換えることで対処しなければなりません。

    // Handle invalid characters by replacing with the Unicode replacement character (U+FFFD)
    ArrayResize(utf8Output, 3); // Resize the array to hold three bytes
    utf8Output[0] = 0xEF; // Store the first byte
    utf8Output[1] = 0xBF; // Store the second byte
    utf8Output[2] = 0xBD; // Store the third byte
    return 3; // Return the length of the UTF-8 representation

まず、utf8Output配列のサイズを3バイトに変更します。これにより、文字を置き換えるための十分なスペースが確保されます。次に、utf8Output配列のバイトをUTF-8表現のU+FFFDに設定します。この文字はUTF-8では0xEF、0xBF、0xBDのバイト列として表示されます。これがutf8Outputに直接割り当てられるストレートバイトで、0xEFが1バイト目、0xBFが2バイト目、0xBDが3バイト目です。最後に3を返しますが、これは置換文字のUTF-8表現が3バイトを占めていることを示します。これは、文字をUTF-8表現に変換できることを確認する完全な関数です。高度なUFT-16を使うこともできますが、これはWebサイトを作る仕事なので、ここではシンプルに考えましょう。したがって、この関数の完全なコードは次のようになります。

//+-----------------------------------------------------------------------+
//| Function to convert a ushort character to its UTF-8 representation    |
//+-----------------------------------------------------------------------+
int ShortToUtf8(const ushort character, uchar &utf8Output[]) {
    // Handle single byte characters (0x00 to 0x7F)
    if (character < 0x80) {
        ArrayResize(utf8Output, 1); // Resize the array to hold one byte
        utf8Output[0] = (uchar)character; // Store the character in the array
        return 1; // Return the length of the UTF-8 representation
    }
    // Handle two-byte characters (0x80 to 0x7FF)
    if (character < 0x800) {
        ArrayResize(utf8Output, 2); // Resize the array to hold two bytes
        utf8Output[0] = (uchar)((character >> 6) | 0xC0); // Store the first byte
        utf8Output[1] = (uchar)((character & 0x3F) | 0x80); // Store the second byte
        return 2; // Return the length of the UTF-8 representation
    }
    // Handle three-byte characters (0x800 to 0xFFFF)
    if (character < 0xFFFF) {
        if (character >= 0xD800 && character <= 0xDFFF) { // Ill-formed characters
            ArrayResize(utf8Output, 1); // Resize the array to hold one byte
            utf8Output[0] = ' '; // Replace with a space character
            return 1; // Return the length of the UTF-8 representation
        }
        else if (character >= 0xE000 && character <= 0xF8FF) { // Emoji characters
            int extendedCharacter = 0x10000 | character; // Extend the character to four bytes
            ArrayResize(utf8Output, 4); // Resize the array to hold four bytes
            utf8Output[0] = (uchar)(0xF0 | (extendedCharacter >> 18)); // Store the first byte
            utf8Output[1] = (uchar)(0x80 | ((extendedCharacter >> 12) & 0x3F)); // Store the second byte
            utf8Output[2] = (uchar)(0x80 | ((extendedCharacter >> 6) & 0x3F)); // Store the third byte
            utf8Output[3] = (uchar)(0x80 | (extendedCharacter & 0x3F)); // Store the fourth byte
            return 4; // Return the length of the UTF-8 representation
        }
        else {
            ArrayResize(utf8Output, 3); // Resize the array to hold three bytes
            utf8Output[0] = (uchar)((character >> 12) | 0xE0); // Store the first byte
            utf8Output[1] = (uchar)(((character >> 6) & 0x3F) | 0x80); // Store the second byte
            utf8Output[2] = (uchar)((character & 0x3F) | 0x80); // Store the third byte
            return 3; // Return the length of the UTF-8 representation
        }
    }
    // Handle invalid characters by replacing with the Unicode replacement character (U+FFFD)
    ArrayResize(utf8Output, 3); // Resize the array to hold three bytes
    utf8Output[0] = 0xEF; // Store the first byte
    utf8Output[1] = 0xBF; // Store the second byte
    utf8Output[2] = 0xBD; // Store the third byte
    return 3; // Return the length of the UTF-8 representation
}

エンコード機能を使用して、メッセージをエンコードして再送信します。

   double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   string msg = "🚀EA INITIALIZED ON CHART " + _Symbol + " 🚀"
                +"\n📊Account Status 📊"
                +"\nEquity: $"
                +DoubleToString(accountEquity,2)
                +"\nFree Margin: $"
                +DoubleToString(accountFreeMargin,2);
   
   string encloded_msg = UrlEncode(msg);
   msg = encloded_msg;

ここでは、URLエンコードされたメッセージを格納するencoded_msgという文字列変数を宣言し、その結果を最初のメッセージに追加しています。そして最後に、結果を最初のメッセージに追加します。これにより、別の変数を宣言するのではなく、技術的にはその内容が上書きされます。これを実行すると、こうなります。

絵文字なしのメッセージ

これは成功だったと見ていいでしょう。構造化された形でメッセージを受け取りました。ただし、最初にメッセージに含まれていた絵文字は破棄されます。これは、エンコードしたからであり、それを取り戻すためには、それぞれのフォーマットを入力しなければなりません。もし削除する必要がないのであれば、それはハードコードしていることを意味するので、関数内の絵文字スニペットを無視すればよいです。ここでは、自動的にエンコードできるように、それぞれのフォーマットで用意しましょう。

   double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   string msg = "\xF680 EA INITIALIZED ON CHART " + _Symbol + "\xF680"
                +"\n\xF4CA Account Status \xF4CA"
                +"\nEquity: $"
                +DoubleToString(accountEquity,2)
                +"\nFree Margin: $"
                +DoubleToString(accountFreeMargin,2);
   
   string encloded_msg = UrlEncode(msg);
   msg = encloded_msg;

「\xF***」の形式で文字を表します。表現に続く単語がある場合、区別のために必ずスペースまたはバックスラッシュ「\」を使用します。つまり、「\xF123」または「\xF123\」です。これを実行すると、次のような結果が得られます。

最後の絵文字を含める

これで、すべての文字が正しくエンコードされた正しいメッセージフォーマットになったことがわかります。成功です。これで本物のシグナルを出すことができます。

WebRequest関数はストラテジーテスター上では動作しないし、移動平均クロスオーバー戦略によるシグナル発生を待つには確認待ちの時間が必要なので、後で移動平均戦略を使うにしても、何か他の手っ取り早い戦略をプログラムの初期化時に使えるように細工しておきましょう。初期化時に前のバーを評価し、強気のバーであれば買い注文を出します。そうでなければ、弱気またはゼロ方向バーの場合、売り注文を出します。これは下図の通りです。

強気と弱気のローソク足

ロジックに使用したコードスニペットは以下の通りです。

   double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   
   double Price_Open = iOpen(_Symbol,_Period,1);
   double Price_Close = iClose(_Symbol,_Period,1);
   
   bool isBuySignal = Price_Open < Price_Close;
   bool isSellSignal = Price_Open >= Price_Close;
   

ここでは、価格提示、つまりAsk価格とBid価格を定義します。次に、iOpen関数を使用して、インデックス 1 にある前のバーの始値を取得します。この関数は、3つの引数またはパラメータ、すなわち商品銘柄、期間、値を取得するバーのインデックスを取ります。終値の取得にはiClose関数が使われます。そして、始値と終値の値を比較するブーリアン変数isBuySignalとisSellSignalを定義し、始値が終値より小さいか、始値が終値以上であれば、それぞれ買いシグナルと売りシグナルのフラグを変数に格納します。

注文を出すにはメソッドが必要です。

#include <Trade/Trade.mqh>
CTrade obj_Trade;

グローバルスコープで、できればコードの先頭で、#includeキーワードを使って取引クラスをインクルードします。これでCTradeクラスにアクセスできるようになります。これを使用して取引オブジェクトを作成します。これは取引を開始するために必要なので、非常に重要です。

CTRADEクラス

プリプロセッサは#include <Trade/Trade.mqh>行をTrade.mqhファイルの内容に置き換えます。角括弧は、Trade.mqhファイルが標準ディレクトリ(通常、terminal_installation_directory\MQL5\Include)から取得されることを示します。カレントディレクトリは検索に含まれません。この行はプログラム中のどこにでも配置できますが、通常は、より良いコード構造と参照を容易にするために、すべてのインクルージョンはソースコードの先頭に置かれます。CTradeクラスのobj_Tradeオブジェクトを宣言すると、MQL5開発者のおかげで、そのクラスに含まれるメソッドに簡単にアクセスできるようになります。

これでポジションを建てることができます。

   double lotSize = 0, openPrice = 0,stopLoss = 0,takeProfit = 0;
   
   if (isBuySignal == true){
      lotSize = 0.01;
      openPrice = Ask;
      stopLoss = Bid-1000*_Point;
      takeProfit = Bid+1000*_Point;
      obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
   }
   else if (isSellSignal == true){
      lotSize = 0.01;
      openPrice = Bid;
      stopLoss = Ask+1000*_Point;
      takeProfit = Ask-1000*_Point;
      obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
   }

取引量、注文の建値、ストップロスレベルとテイクプロフィットレベルを格納するdouble変数を定義し、それらをゼロに初期化します。ポジションを建てるには、まずisBuySignalにtrueのフラグがあるかどうか、つまり前のバーが本当に強気であったかどうかをチェックし、それから買いポジションを建てます。ロットサイズは0.01に初期化され、建値はask価格、ストップロスとテイクプロフィットレベルはbid価格から計算され、その結果が買いポジションを建てるために使用されます。同様に、売りポジションを建てるために、値が計算され、関数で使用されます。

ポジションが建てられたら、発生したシグナルと建てたポジションの情報を1つのメッセージにまとめ、Telegramに中継することができます。

   string position_type = isBuySignal ? "Buy" : "Sell";
   
   ushort MONEYBAG = 0xF4B0;
   string MONEYBAG_Emoji_code = ShortToString(MONEYBAG);
   string msg =  "\xF680 OPENED "+position_type+" POSITION."
          +"\n===================="
          +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits)
          +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS)
          +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS)
          +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2)
          +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits)
          +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits)
          +"\n_________________________"
          +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE)
          +" @ "+TimeToString(TimeLocal(),TIME_SECONDS)
          ;
   string encloded_msg = UrlEncode(msg);
   msg = encloded_msg;
   

ここでは、売買シグナルに関連する情報を含む明確で正確なメッセージを作成します。絵文字やその他の関連データを用いて、受信者が情報を消化しやすいようにメッセージをフォーマットします。シグナルが「買い」ベースのシグナルか「売り」ベースのシグナルかを判断することから始めます。これには三項演算子を使います。そして、「買い」または「売り」のシグナルにふさわしいと思われる札束の絵文字を含むメッセージを作成します。実際の絵文字表現文字をushortフォーマットで使用し、後でその文字コードをShortToString関数を使って文字列変数に変換しました。これは、常に文字列形式を使用する必要がないことを単純に示しています。しかし、各文字に名前を付けたいのであれば、この方法がベストではあるが、変換処理には時間とスペースがかかることがお分かりいただけるでしょう。

そして、未決済の取引ポジションの情報を文字列にまとめます。この文字列がメッセージに変換されると、取引の詳細(取引の種類、始値、取引時間、現在時刻、ロットサイズ、ストップロス、テイクプロフィットなど)が含まれます。メッセージを視覚的にアピールし、解釈しやすくする方法でこれをおこないます。

メッセージの構成に続いて、UrlEncode関数を呼び出し、URLへの安全な送信のためにメッセージをエンコードします。特に、すべての特殊文字と絵文字が正しく扱われ、Webに適合していることを保証します。そして、エンコードされたメッセージをencoded_msgという変数に格納し、エンコードされたメッセージを最初のメッセージで上書きします。これを実行すると、次のような結果が得られます。

最終初期化シグナルメッセージ

目的構造でメッセージをエンコードし、Telegramに送信することに成功していることがわかります。これを送信するソースコードの全文は以下の通りです。

//+------------------------------------------------------------------+
//|                                  TELEGRAM_MQL5_SIGNALS_PART2.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include <Trade/Trade.mqh>
CTrade obj_Trade;

// Define constants for Telegram API URL, bot token, and chat ID
const string TG_API_URL = "https://api.telegram.org";  // Base URL for Telegram API
const string botTkn = "7456439661:AAELUurPxI1jloZZl3Rt-zWHRDEvBk2venc";  // Telegram bot token
const string chatID = "-4273023945";  // Chat ID for the Telegram chat

// The following URL can be used to get updates from the bot and retrieve the chat ID
// CHAT ID = https://api.telegram.org/bot{BOT TOKEN}/getUpdates
// https://api.telegram.org/bot7456439661:AAELUurPxI1jloZZl3Rt-zWHRDEvBk2venc/getUpdates


//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {

   char data[];  // Array to hold data to be sent in the web request (empty in this case)
   char res[];  // Array to hold the response data from the web request
   string resHeaders;  // String to hold the response headers from the web request
   //string msg = "EA INITIALIZED ON CHART " + _Symbol;  // Message to send, including the chart symbol
   ////--- Simple Notification with Emoji:
   //string msg = "🚀 EA INITIALIZED ON CHART " + _Symbol + " 🚀";
   ////--- Buy/Sell Signal with Emoji:
   //string msg = "📈 BUY SIGNAL GENERATED ON " + _Symbol + " 📈";
   //string msg = "📉 SELL SIGNAL GENERATED ON " + _Symbol + " 📉";
   ////--- Account Balance Notification:
   //double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   //string msg = "💰 Account Balance: $" + DoubleToString(accountBalance, 2) + " 💰";
   ////--- Trade Opened Notification:
   //string orderType = "BUY";  // or "SELL"
   //double lotSize = 0.1;  // Example lot size
   //double price = 1.12345;  // Example price
   //string msg = "🔔 " + orderType + " order opened on " + _Symbol + "; Lot size: " + DoubleToString(lotSize, 2) + "; Price: " + DoubleToString(price, 5) + " 🔔";
   ////--- Stop Loss and Take Profit Update:
   //double stopLoss = 1.12000;  // Example stop loss
   //double takeProfit = 1.13000;  // Example take profit
   //string msg = "🔄 Stop Loss and Take Profit Updated on " + _Symbol + "; Stop Loss: " + DoubleToString(stopLoss, 5) + "; Take Profit: " + DoubleToString(takeProfit, 5) + " 🔄";
   ////--- Daily Performance Summary:
   //double profitToday = 150.00;  // Example profit for the day
   //string msg = "📅 Daily Performance Summary 📅; Symbol: " + _Symbol + "; Profit Today: $" + DoubleToString(profitToday, 2);
   ////--- Trade Closed Notification:
   //string orderType = "BUY";  // or "SELL"
   //double profit = 50.00;  // Example profit
   //string msg = "❌ " + orderType + " trade closed on " + _Symbol + "; Profit: $" + DoubleToString(profit, 2) + " ❌";
   
//   ////--- Account Status Update:
//   double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
//   double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
//   string msg = "\xF680 EA INITIALIZED ON CHART " + _Symbol + "\xF680"
//                +"\n\xF4CA Account Status \xF4CA"
//                +"\nEquity: $"
//                +DoubleToString(accountEquity,2)
//                +"\nFree Margin: $"
//                +DoubleToString(accountFreeMargin,2);
//   
//   string encloded_msg = UrlEncode(msg);
//   msg = encloded_msg;

   double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   
   double Price_Open = iOpen(_Symbol,_Period,1);
   double Price_Close = iClose(_Symbol,_Period,1);
   
   bool isBuySignal = Price_Open < Price_Close;
   bool isSellSignal = Price_Open >= Price_Close;
   
   double lotSize = 0, openPrice = 0,stopLoss = 0,takeProfit = 0;
   
   if (isBuySignal == true){
      lotSize = 0.01;
      openPrice = Ask;
      stopLoss = Bid-1000*_Point;
      takeProfit = Bid+1000*_Point;
      obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
   }
   else if (isSellSignal == true){
      lotSize = 0.01;
      openPrice = Bid;
      stopLoss = Ask+1000*_Point;
      takeProfit = Ask-1000*_Point;
      obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
   }
   
   string position_type = isBuySignal ? "Buy" : "Sell";
   
   ushort MONEYBAG = 0xF4B0;
   string MONEYBAG_Emoji_code = ShortToString(MONEYBAG);
   string msg =  "\xF680 OPENED "+position_type+" POSITION."
          +"\n===================="
          +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits)
          +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS)
          +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS)
          +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2)
          +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits)
          +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits)
          +"\n_________________________"
          +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE)
          +" @ "+TimeToString(TimeLocal(),TIME_SECONDS)
          ;
   string encloded_msg = UrlEncode(msg);
   msg = encloded_msg;
   
   // Construct the URL for the Telegram API request to send a message
   // Format: https://api.telegram.org/bot{HTTP_API_TOKEN}/sendmessage?chat_id={CHAT_ID}&text={MESSAGE_TEXT}
   const string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID +
      "&text=" + msg;

   // Send the web request to the Telegram API
   int send_res = WebRequest("POST", url, "", 10000, data, res, resHeaders);

   // Check the response status of the web request
   if (send_res == 200) {
      // If the response status is 200 (OK), print a success message
      Print("TELEGRAM MESSAGE SENT SUCCESSFULLY");
   } else if (send_res == -1) {
      // If the response status is -1 (error), check the specific error code
      if (GetLastError() == 4014) {
         // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal
         Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL");
      }
      // Print a general error message if the request fails
      Print("UNABLE TO SEND THE TELEGRAM MESSAGE");
   } else if (send_res != 200) {
      // If the response status is not 200 or -1, print the unexpected response code and error code
      Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError());
   }

   return(INIT_SUCCEEDED);  // Return initialization success status
}

次に、移動平均のクロスオーバーに基づく売買シグナルを含める必要があります。まず、2つの移動平均指標ハンドルとそのデータストレージ配列を宣言する必要があります。

int handleFast = INVALID_HANDLE; // -1
int handleSlow = INVALID_HANDLE; // -1

double bufferFast[];
double bufferSlow[];

long magic_no = 1234567890;

まず、handleFastとhandleSlowという名前の整数データ型変数を宣言し、それぞれ動きの速い平均指標と遅い平均指標を格納します。ハンドルをINVALID_HANDLE(-1)に初期化し、有効な指標インスタンスを参照していないことを示します。bufferFastとbufferSlow という2つのdouble配列を定義し、それぞれfastとslowの指標から取得した値を格納します。最後に、建てたポジションのマジックナンバーを格納するlong変数を宣言します。このロジックはすべてグローバルスコープに置かれます。

OnInit関数で、指標ハンドルを初期化し、ストレージ配列を時系列として設定します。 

   handleFast = iMA(Symbol(),Period(),20,0,MODE_EMA,PRICE_CLOSE);
   if (handleFast == INVALID_HANDLE){
      Print("UNABLE TO CREATE FAST MA INDICATOR HANDLE. REVERTING NOW!");
      return (INIT_FAILED);
   }

ここでは、高速移動平均指標のハンドルを作成します。これは、Symbol、Period、20、0、MODE_EMA、PRICE_CLOSEをパラメータとして呼び出されるiMA関数を使用しておこなわれます。最初のパラメータ「 Symbol」は、現在の商品の名前を返す組み込み関数です。2番目のパラメータ「Period」は、現在の時間枠を返します。次のパラメータ「20」は、移動平均の期間数です。4番目のパラメータ「0」は、移動平均を直近の価格バーに適用することを示します。5番目のパラメータ「MODE_EMA」は、指数移動平均(EMA)を計算したいことを示します。最後のパラメータは「PRICE_CLOSE」で、これは終値に基づいて移動平均を計算することを示しています。この関数は、この移動平均指標のインスタンスを一意に識別するハンドルを返し、それをhandleFastに代入します。

指標を作成しようとしたら、ハンドルが有効かどうかを確認します。handleFastのINVALID_HANDLEという結果は、高速移動平均指標のハンドルを作成できなかったことを示します。この場合、重大度レベルERRORのメッセージをログに出力します。ユーザー宛のメッセージには、プログラムが「UNABLE TO CREATE FAST MA INDICATOR HANDLE.REVERTING NOW!」と表示します。ハンドルがないことは指標がないことを意味し、これは指標のハンドルを作成できなかったことを意味します。この指標がなければ、取引システムは存在せず、プログラムは無意味となるため、この指標を使い続ける意味はありません。障害が発生したため、それ以上処理を進めずに「INIT_FAILED」を返します。これにより、プログラムはそれ以上実行されなくなり、チャートから削除されます。

同じロジックが低速指標にも当てはまります。

   handleSlow = iMA(Symbol(),Period(),50,0,MODE_SMA,PRICE_CLOSE);
   if (handleSlow == INVALID_HANDLE){
      Print("UNABLE TO CREATE FAST MA INDICATOR HANDLE. REVERTING NOW!");
      return (INIT_FAILED);
   }

これらの指標ハンドルを印刷すると、開始値は10となり、さらに指標ハンドルがあれば、その値はハンドルごとに1ずつ増加します。出力して、何が出てくるか見てみましょう。これは以下のコードで実現できます。

   Print("HANDLE FAST MA = ",handleFast);
   Print("HANDLE SLOW MA = ",handleSlow);

次のような出力が得られます。

指標ハンドル出力

最後に、データストレージ配列を時系列に設定し、マジックナンバーを設定します。

   ArraySetAsSeries(bufferFast,true);
   ArraySetAsSeries(bufferSlow,true);
   obj_Trade.SetExpertMagicNumber(magic_no);

配列を時系列に設定するには、ArraySetAsSeries関数を使用します。

OnDeinit関数では、IndicatorRelease関数を使用してコンピュータメモリーから指標ハンドルを解放し、ArrayFree関数を使用してストレージ配列を解放します。これにより、不要なプロセスをコンピュータから解放し、リソースを確保することができます。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   // Code to execute when the expert is deinitialized
   
   IndicatorRelease(handleFast);
   IndicatorRelease(handleSlow);
   ArrayFree(bufferFast);
   ArrayFree(bufferSlow);
   
}

OnTickイベントハンドラで、指標のハンドルを利用するコードを実行し、シグナルの発生をチェックします。これは、ティック毎、つまり価格相場の変化毎に呼び出され、最新の価格を取得する関数です。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   // Code to execute on every tick event

   ...

}

これは、指標の値を取得する必要があるイベントハンドラです。

   if (CopyBuffer(handleFast,0,0,3,bufferFast) < 3){
      Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING");
      return;
   }

まず、CopyBuffer関数を使って、高速移動する平均指標バッファからデータを取得しようとします。handleFast、0、0、3、bufferFastというパラメータで呼び出します。最初のパラメータ「handleFast」は、指標値を取得する対象の指標です。2つ目のパラメータはバッファ番号で、通常はデータウィンドウに表示される値を取得する場所であり、移動平均の場合は常に0です。3番目のパラメータは、値を取得するバーインデックスの開始位置で、この場合0は現在のバーを意味します。4番目のパラメータは、取得する値の数、つまりバー数です。この場合の3は、現在のバーから最初の3バーを意味します。最後のパラメータはbufferFastで、これは3つの取得値を格納するターゲット配列です。

ここで、この関数が要求された値、つまり3の値を正常に取得できたかどうかをチェックします。返された値が3より小さければ、その関数は要求されたデータを取得できなかったことを示します。このような場合、「UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS.REVERTING.」というエラーメッセージが表示されます。これはデータ検索が失敗し処理に必要なデータが不足しているため、シグナルのスキャンを続けることができないことを通知するものです。そして戻り、プログラムのこの部分の実行を止め、次のティックを待ちます。

同じプロセスで、動きの遅い平均のデータを取り出します。

   if (CopyBuffer(handleSlow,0,0,3,bufferSlow) < 3){
      Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING");
      return;
   }

OnTick関数はティックごとに実行されるため、シグナルスキャンコードをバーごとに1回実行するようにロジックを開発する必要があります。ロジックはこうです。

   int currBars = iBars(_Symbol,_Period);
   static int prevBars = currBars;
   if (prevBars == currBars) return;
   prevBars = currBars;

まず、指定された取引銘柄と期間、または聞いたことがあるかもしれませんが時間枠のチャート上の現在のバーの計算された数を格納する整数変数「currBars」を宣言します。これは、iBars関数を使用することで実現できます。この関数は2つの引数、つまり銘柄と期間だけを取ります。 

次に、新しいバーが生成されたときにチャート上の前のバーの合計数を格納するために、別の静的整数変数prevBarsを宣言し、関数の最初の実行時にチャート上の現在のバーの値で初期化します。現在のバー数と前回のバー数を比較し、チャート上で新しいバーが生成されるインスタンスを決定するために使用します。

最後に、条件文を使用して、現在のバー数と前回のバー数が等しいかどうかを確認します。両者が等しい場合は、新しいバーが形成されていないことを意味するので、それ以降の実行を終了して戻ります。現在のバーカウントと前のバーカウントが等しくなければ、新しいバーが形成されたことを示します。この場合、prevBars変数をcurrBarsに更新し、次のティックでは、prevBars変数がチャート上のバーの数と同じになるようにします。

次に、さらなる分析のために、データを簡単に保存できる変数を以下のように定義します。

   double fastMA1 = bufferFast[1];
   double fastMA2 = bufferFast[2];
   
   double slowMA1 = bufferSlow[1];
   double slowMA2 = bufferSlow[2];

これらの変数があれば、クロスオーバーをチェックし、必要な措置を講じることができます。

   if (fastMA1 > slowMA1 && fastMA2 <= slowMA2){
      for (int i = PositionsTotal()-1; i>= 0; i--){
         ulong ticket = PositionGetTicket(i);
         if (ticket > 0){
            if (PositionSelectByTicket(ticket)){
               if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
                  PositionGetInteger(POSITION_MAGIC) == magic_no){
                  if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
                     obj_Trade.PositionClose(ticket);
                  }
               }
            }
         }
      }
      double lotSize = 0.01;
      double openPrice = Ask;
      double stopLoss = Bid-1000*_Point;
      double takeProfit = Bid+1000*_Point;
      obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
   }

直近の高速移動平均線(fastMA1)が対応する低速移動平均線(slowMA1)よ りも大きく、直前の高速移動平均線(fastMA2)が直前の低速移動平均線 (slowMA2)以下であれば、強気のクロスオーバーであり、買いシグナルの可能性があります。 

強気のクロスオーバーが確認されると、現在のポジションをループして、新たな買いの邪魔になる売りポジションがないかをチェックします。必要であれば、売りポジションを決済してから新たな買いを建てます。直近のポジションから、直近でないポジションへ作業します。

各取引ポジションについて、PositionGetTicket 関数を使用してチケット番号を取得します。チケット番号が0より大きい場合、つまり有効なチケット番号を持っており、PositionSelectByTicket関数でポジションを選択した場合、ポジションが有効かどうかのチェックを続け、現在の銘柄とマジック番号に属するかどうかを確認します。ポジションが売りポジションの場合、obj_Trade.PositionClose関数を使用してポジションをクローズします。既存の売りポジションをすべて決済した後、新規の買いポジションを建て、取引パラメータ(ロットサイズ、建値、ストップロス、利食い)を設定します。ポジションが建てられると、操作ログにログを送信することで、インスタンスをユーザーに知らせます。

      // BUY POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM
      Print("BUY POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW.");

最後に、プログラムの初期化セクションで行ったのと同じように、メッセージを送信します。

      ushort MONEYBAG = 0xF4B0;
      string MONEYBAG_Emoji_code = ShortToString(MONEYBAG);
      string msg =  "\xF680 Opened Buy Position."
             +"\n===================="
             +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits)
             +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS)
             +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS)
             +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2)
             +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits)
             +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits)
             +"\n_________________________"
             +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE)
             +" @ "+TimeToString(TimeLocal(),TIME_SECONDS)
             ;
      string encloded_msg = UrlEncode(msg);
      msg = encloded_msg;

売りのクロスオーバーシグナルについては、逆条件で同じコード構造のままです。

   else if (fastMA1 < slowMA1 && fastMA2 >= slowMA2){
      for (int i = PositionsTotal()-1; i>= 0; i--){
         ulong ticket = PositionGetTicket(i);
         if (ticket > 0){
            if (PositionSelectByTicket(ticket)){
               if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
                  PositionGetInteger(POSITION_MAGIC) == magic_no){
                  if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
                     obj_Trade.PositionClose(ticket);
                  }
               }
            }
         }
      }
      double lotSize = 0.01;
      double openPrice = Bid;
      double stopLoss = Ask+1000*_Point;
      double takeProfit = Ask-1000*_Point;
      obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
      
      // SELL POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM
      Print("SELL POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW.");

ここまでで、コード構造はほぼ完成しました。あとは、可視化のためにプログラムがロードされたら、指標を自動的にチャートに追加するだけです。そこで、初期化イベントハンドラで、以下のように指標を自動的に追加するロジックを作成します。

   //--- Add indicators to the chart automatically
   ChartIndicatorAdd(0,0,handleFast);
   ChartIndicatorAdd(0,0,handleSlow);

ここでは、ChartIndicatorAdd関数を呼び出してチャートに指標を追加します。最初のパラメータと2番目のパラメータは、それぞれチャートウィンドウとサブウィンドウを指定します。3番目のパラメータは、追加する指標ハンドルです。

したがって、シグナルの生成とチャネリングを担当するOnTickイベントハンドラの完全なコードは以下のようになります。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   // Code to execute on every tick event
   
   if (CopyBuffer(handleFast,0,0,3,bufferFast) < 3){
      Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING");
      return;
   }
   if (CopyBuffer(handleSlow,0,0,3,bufferSlow) < 3){
      Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING");
      return;
   }
   
   double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   
   int currBars = iBars(_Symbol,_Period);
   static int prevBars = currBars;
   if (prevBars == currBars) return;
   prevBars = currBars;
   
   double fastMA1 = bufferFast[1];
   double fastMA2 = bufferFast[2];
   
   double slowMA1 = bufferSlow[1];
   double slowMA2 = bufferSlow[2];
   
   if (fastMA1 > slowMA1 && fastMA2 <= slowMA2){
      for (int i = PositionsTotal()-1; i>= 0; i--){
         ulong ticket = PositionGetTicket(i);
         if (ticket > 0){
            if (PositionSelectByTicket(ticket)){
               if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
                  PositionGetInteger(POSITION_MAGIC) == magic_no){
                  if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
                     obj_Trade.PositionClose(ticket);
                  }
               }
            }
         }
      }
      double lotSize = 0.01;
      double openPrice = Ask;
      double stopLoss = Bid-1000*_Point;
      double takeProfit = Bid+1000*_Point;
      obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
      
      // BUY POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM
      Print("BUY POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW.");
      
      char data[];  // Array to hold data to be sent in the web request (empty in this case)
      char res[];  // Array to hold the response data from the web request
      string resHeaders;  // String to hold the response headers from the web request
      
      
      ushort MONEYBAG = 0xF4B0;
      string MONEYBAG_Emoji_code = ShortToString(MONEYBAG);
      string msg =  "\xF680 Opened Buy Position."
             +"\n===================="
             +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits)
             +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS)
             +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS)
             +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2)
             +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits)
             +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits)
             +"\n_________________________"
             +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE)
             +" @ "+TimeToString(TimeLocal(),TIME_SECONDS)
             ;
      string encloded_msg = UrlEncode(msg);
      msg = encloded_msg;
   
      const string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID +
         "&text=" + msg;
   
      // Send the web request to the Telegram API
      int send_res = WebRequest("POST", url, "", 10000, data, res, resHeaders);
   
      // Check the response status of the web request
      if (send_res == 200) {
         // If the response status is 200 (OK), print a success message
         Print("TELEGRAM MESSAGE SENT SUCCESSFULLY");
      } else if (send_res == -1) {
         // If the response status is -1 (error), check the specific error code
         if (GetLastError() == 4014) {
            // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal
            Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL");
         }
         // Print a general error message if the request fails
         Print("UNABLE TO SEND THE TELEGRAM MESSAGE");
      } else if (send_res != 200) {
         // If the response status is not 200 or -1, print the unexpected response code and error code
         Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError());
      }

      
   }
   else if (fastMA1 < slowMA1 && fastMA2 >= slowMA2){
      for (int i = PositionsTotal()-1; i>= 0; i--){
         ulong ticket = PositionGetTicket(i);
         if (ticket > 0){
            if (PositionSelectByTicket(ticket)){
               if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
                  PositionGetInteger(POSITION_MAGIC) == magic_no){
                  if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
                     obj_Trade.PositionClose(ticket);
                  }
               }
            }
         }
      }
      double lotSize = 0.01;
      double openPrice = Bid;
      double stopLoss = Ask+1000*_Point;
      double takeProfit = Ask-1000*_Point;
      obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
      
      // SELL POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM
      Print("SELL POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW.");
      
      char data[];  // Array to hold data to be sent in the web request (empty in this case)
      char res[];  // Array to hold the response data from the web request
      string resHeaders;  // String to hold the response headers from the web request
   
      ushort MONEYBAG = 0xF4B0;
      string MONEYBAG_Emoji_code = ShortToString(MONEYBAG);
      string msg =  "\xF680 Opened Sell Position."
             +"\n===================="
             +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits)
             +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS)
             +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS)
             +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2)
             +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits)
             +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits)
             +"\n_________________________"
             +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE)
             +" @ "+TimeToString(TimeLocal(),TIME_SECONDS)
             ;
      string encloded_msg = UrlEncode(msg);
      msg = encloded_msg;
   
      const string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID +
         "&text=" + msg;
   
      // Send the web request to the Telegram API
      int send_res = WebRequest("POST", url, "", 10000, data, res, resHeaders);
   
      // Check the response status of the web request
      if (send_res == 200) {
         // If the response status is 200 (OK), print a success message
         Print("TELEGRAM MESSAGE SENT SUCCESSFULLY");
      } else if (send_res == -1) {
         // If the response status is -1 (error), check the specific error code
         if (GetLastError() == 4014) {
            // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal
            Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL");
         }
         // Print a general error message if the request fails
         Print("UNABLE TO SEND THE TELEGRAM MESSAGE");
      } else if (send_res != 200) {
         // If the response status is not 200 or -1, print the unexpected response code and error code
         Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError());
      }
      
   }
   
}
//+------------------------------------------------------------------+

これで、第2の目的、つまり取引端末からTelegramのチャットやグループにシグナルを送るという目的を達成したことは明らかです。これは成功です。次にやるべきことは、統合が正しく機能することを確認し、発生する問題を特定するためのテストです。これは次のセクションで説明します。


統合のテスト

統合をテストするために、初期化テストロジックをコメントアウトして無効にし、多くのシグナルがオープンしないようにし、期間を1分足と低めにシフトし、指標の期間を5と10に変更し、シグナルを素早く発生させます。以下がそのマイルストーンです。

取引端末の売りシグナル確認

MT5売りシグナル

Telegramの売りシグナル確認

Telegram売りシグナル

取引端末の買いシグナル確認

MT5買いシグナル

Telegramの買いシグナル確認

Telegram買いシグナル

画像から、統合がうまくいっていることがわかります。シグナルスキャンがあり、それが確認されると、その詳細が1つのメッセージにエンコードされ、取引端末からTelegramグループチャットに送信されます。こうして、目的を達成することに成功しました。


結論

この記事では、取引端末からTelegramチャットへ直接売買シグナルを送信するという主な目標を達成し、MQL5-Telegram統合EAの開発において大きな進展を遂げました。しかし、第1回でおこなったようにMQL5とTelegram間の通信チャネルを確立するだけにとどまらず、移動平均クロスオーバーという人気の高いテクニカル分析手法を活用し、実際の売買シグナルに注目しました。そのシグナルのロジックを詳しく解説し、Telegramを通じてシグナルを送信するための堅牢なシステムも構築しました。結果として、統合EA全体の設定が大幅に進化しました。

この記事では、シグナルの生成と送信の方法を技術的に深く掘り下げ、メッセージの安全なエンコード方法、指標のハンドリング、シグナルに基づく取引実行の手順について解説しました。コードを作成し、Telegramコとの統合を実現することで、取引プラットフォームから離れていてもリアルタイムで売買シグナルを受け取れるようにしました。この記事の実践的な例と詳細な説明は、読者が自身の取引戦略をもとに同様のシステムを設定するための明確な指針を提供するはずです。

次回の第3回では、MQL5とTelegramの統合にさらに一歩進み、にチャートのスクリーンショットを送信するソリューションに取り組みます。これにより、市場とシグナルの背景を視覚的に分析でき、トレーダーの理解と判断力を強化します。視覚データとテキストシグナルの組み合わせは、より強力なシグナル提供に繋がるでしょう。私たちの目指すのは、シグナルの送信だけでなく、Telegram取引チャネルを通じて自動取引と状況認識をさらに強化することです。ご期待ください。


MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/15495

知っておくべきMQL5ウィザードのテクニック(第31回):損失関数の選択 知っておくべきMQL5ウィザードのテクニック(第31回):損失関数の選択
損失関数は、機械学習アルゴリズムの重要な指標です。これは、与えられたパラメータセットが目標に対してどれだけうまく機能しているかを定量的に評価し、学習プロセスにフィードバックを提供する役割を果たします。本記事では、MQL5のカスタムウィザードクラスを使って、損失関数のさまざまな形式を探っていきます。
MQL5-Telegram統合エキスパートアドバイザーの作成(第1回):MQL5からTelegramへのメッセージ送信 MQL5-Telegram統合エキスパートアドバイザーの作成(第1回):MQL5からTelegramへのメッセージ送信
この記事では、MQL5を使用してEAを作成し、Telegramに自動でメッセージを送信する方法を説明します。ボットのAPIトークンやチャットIDといった必要なパラメータを設定し、HTTP POSTリクエストを実行してメッセージを配信する流れを学びます。また、応答を処理し、万が一メッセージ送信が失敗した場合には、トラブルシューティングについても解説します。最終的には、MQL5を通じてTelegramにメッセージを送るボットを構築する手順をマスターします。
SMAとEMAを使った自動最適化された利益確定と指標パラメータの例 SMAとEMAを使った自動最適化された利益確定と指標パラメータの例
この記事では、機械学習とテクニカル分析を組み合わせた、FX取引向けの高度なEAを紹介します。アップル株取引を中心に、適応的な最適化やリスク管理、複数の取引戦略を活用しています。バックテストでは、収益性が高い一方で、大きなドローダウンを伴う結果が得られており、さらなる改良の余地が示唆されています。
データサイエンスと機械学習(第29回):AI訓練に最適なFXデータを選ぶための重要なヒント データサイエンスと機械学習(第29回):AI訓練に最適なFXデータを選ぶための重要なヒント
この記事では、AIモデルのパフォーマンスを向上させるために、最も適切で高品質なFXデータを選択するための重要な側面について深く掘り下げます。