English Русский 中文 Español Deutsch Português
preview
MQL5-Telegram統合エキスパートアドバイザーの作成(第3回):MQL5からTelegramにキャプション付きチャートのスクリーンショットを送信する

MQL5-Telegram統合エキスパートアドバイザーの作成(第3回):MQL5からTelegramにキャプション付きチャートのスクリーンショットを送信する

MetaTrader 5トレーディングシステム |
143 18
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の第2回では、シグナル生成と転送のためにMetaQuotes Language 5(MQL5)とTelegramを連携させる手順を詳しく検証しました。その結果、Telegramに売買シグナルを送信することができるようになり、シグナルを利用する基盤が整いました。では、なぜさらに統合を進める必要があるのでしょうか。この第3回では、MQL5とTelegramを「次のステップ」に進め、売買シグナルのテキストだけでなく、チャートのスクリーンショットも送信できるようにします。シグナルを受信して行動するだけでなく、プライスアクションの設定など、シグナルの視覚的な内容を直接確認できるのは、チャート画像を提供する大きなメリットです。

この記事では、画像データをHyperText Transfer Protocol Secure(HTTPS)リクエストに埋め込める互換性のあるフォーマットに変換する方法に焦点を当て、Telegramボットに画像を含めるために必要なステップを解説します。MQL5のチャートから、MetaTrader 5取引端末を経由し、キャプションとチャート画像を含む視覚的に印象的なボットメッセージとして取引通知が表示されるまでの流れを紹介します。この記事は以下の4つのパートで構成されています。

はじめに、画像エンコードとHTTPS経由での送信方法についての基本的な概念とテクニックを紹介します。この最初のセクションでは、このタスクを達成するための基本的な概念とテクニックについて詳しく解説します。次に、MetaTrader 5プラットフォーム用の取引プログラムを記述するために使用されるプログラミング言語であるMQL5での実装プロセスに移ります。ここでは、画像エンコードと伝送方法の具体的な使い方を詳しく説明します。その後、実装したプログラムが正しく動くかどうかをテストし、動作の確認をおこないます。そして、最後にもう一度要点をまとめ、この作業を取引システムでおこなうことの利点を述べます。ここでは、EAを作成するための重要なトピックについて紹介します。

  1. 画像のエンコードとHTTPSでの送信の概要
  2. MQL5での実装
  3. 統合のテスト
  4. 結論

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


画像のエンコードとHTTPSでの送信の概要

インターネットを介して画像を送信する場合、特にAPI(アプリケーションプログラミングインターフェイス)やメッセージングプラットフォームとの統合においては、まず画像データをエンコードし、過度な遅延や性能低下、セキュリティリスクを最小化しながら送信する必要があります。直接的な画像ファイルの送信では、インターネットユーザーが特定のWebサイトやプラットフォーム、サービスにアクセスするためのコマンドをスムーズに実行するには、過大なビットやバイトが送信されるため不適切です。TelegramのようなAPIは、インターネットユーザーと特定のサービス(様々なタスクに対応するWebベースのインターフェイス)を仲介する役割を果たすため、画像送信の際には、まず画像ファイルをエンコードし、ユーザーからサービスへ、またはその逆に送信されるコマンドのペイロードの一部として送信する必要があります。これは、特にHTTPやHTTPSといったプロトコルにより実現されます。

画像を送信用に変換する最も一般的な方法は、画像をBase64文字列に変換することです。Base64のエンコーディングでは、画像のバイナリデータをテキスト表現に変換し、特定の文字セットを用いて「エンコードされた画像」としてテキストプロトコル上で送信可能な形式にします。Base64エンコードをおこなうには、画像ファイルの生データをバイト単位で読み取り、そのバイトデータをBase64表記で再構成します。こうして作成されたエンコードファイルは、テキストプロトコルでの送信が可能となります。

エンコードされた画像データは、HTTPの安全な形式であるHTTPSで送信されます。HTTPがプレーンテキストでデータを送信するのに対し、HTTPSはSSL (Secure Socket Layer)やTLS (Transport Layer Security)といった暗号化プロトコルを使用し、サーバー間のデータ通信をプライベートで安全に保ちます。取引シグナルや金融に関連する通信を保護する上で、HTTPSの重要性は非常に高いと言えます。例えば、取引シグナルが悪意のある第三者に傍受されると、その情報が悪用され、取引シグナルの受信者に不利益を与えたり、市場が傍受者に有利になるよう操作されたりするリスクが生じます。プロセスの可視化は以下の通りです。

画像符号化処理

要約すると、画像のエンコードと送信方法は、画像ファイルをWeb通信に適したテキスト形式に変換し、さらにHTTPSを使用して安全に配信する仕組みです。画像データをアプリケーションに統合するには、この2つの基本概念を理解することが重要です。明確な例として、Telegramプラットフォームを介して通知を自動化する取引システムがあり、迅速かつ確実にメッセージを配信する効果的な手段となっています。


MQL5での実装

MQL5での画像リレーの実装は、EA内で現在の取引チャートのスクリーンショットをキャプチャするところから始まります。キャプチャしたスクリーンショットをエンコードし、Telegramに送信します。このプロセスは、主にEAが初期化される際に実行されるOnInitイベントハンドラで設定します。OnInitイベントハンドラの役割は、EAに必要なコンポーネントの準備とセットアップをおこない、取引操作のメインロジックが実行される前に、すべてが正しく整っていることを確認することです。まず、スクリーンショット画像のファイル名を定義します。

   //--- Get ready to take a chart screenshot of the current chart
   
   #define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.jpg"

ここでは、スクリーンショットファイルの名前に定数を設定するという実装の最初のステップを踏んでいます。#defineディレクティブを使用することで、コード全体で参照可能な定数値を割り当てることができます。ここではSCREENSHOT_FILE_NAMEという定数を作成し、「Our Chart ScreenShot.jpg」という値を格納しています。これはもっともなメリットがあります。これにより、ファイルの読み込みや保存が必要な際に、この定数を参照するだけで済みます。ファイル名やフォーマットを変更する必要がある場合も、この1か所を変更するだけで済むのです。今回は画像形式として (Joint Photographic Experts Group)を使用していますが、PNG (Portable Network Graphics)など、他のフォーマットを選ぶことも可能です。ただし、画像フォーマットにはそれぞれ異なる特徴があることを念頭に置く必要があります。例えば、JPEGは非可逆圧縮アルゴリズムを使用しており、多少のデータが失われる代わりに画像サイズが縮小されます。以下に使用可能なフォーマットの例を示します。

画像フォーマット

スクリーンショット関数をOnInitハンドラに統合することで、EAが起動すると同時にチャートの状態をキャプチャして保存できるようにします。ここで定義した定数SCREENSHOT_FILE_NAMEを用いて、チャート画像ファイルの名前を指定しています。このプレースホルダーを使用することで、同じ名前でのファイルがほぼ同時に保存されるリスクを回避できます。これにより、チャート画像ファイルの構造が統一され、画像のエンコードや送信がスムーズにおこなえるようになります。

このステップは、ファイル命名規則を定め、EAが初期化された際にチャート画像を確実に取得できるようにするために重要です。これにより、チャートからのデータを取り出し、人間の目に適した形にエンコードして選択したTelegramチャネルへ送信する準備が整います。

また、同じファイル名で画像を作成しようとした際、既存のファイルがあると上書きされる可能性があるため、まず既存のファイルを削除し、新しいファイルを作成することが必要です。これは以下のコードスニペットで実現できます。

   //--- First delete an instance of the screenshot file if it already exists
   if(FileIsExist(SCREENSHOT_FILE_NAME)){
      FileDelete(SCREENSHOT_FILE_NAME);
      Print("Chart Screenshot was found and deleted.");
      ChartRedraw(0);
   }

まず、新しいスクリーンショットをキャプチャする前に、既存のスクリーンショットファイルが存在しないことを確認することが重要です。これにより、現在のチャートの状態と過去に保存されたスクリーンショットが混同されるのを防ぐことができます。この確認にはFileIsExist、FileIsExist関数を使用し、SCREENSHOT_FILE_NAMEという定数に格納された名前のファイルが存在するかどうかをチェックします。ファイルが見つかればtrueが返されるため、次にFileDelete関数を使用してそのファイルを削除します。指定したディレクトリに古いスクリーンショットがないことを確認することで、後のプロセスで新しいスクリーンショットを保存するためのスペースを確保できます。

削除が完了したら、Print関数で端末にメッセージを出力し、以前のスクリーンショットが見つかり、正常に削除されたことを通知します。このフィードバックは、古いスクリーンショットの削除が適切に行われているかを確認できるため、デバッグ時にも役立ちます。また、存在しないファイルを「削除」しようとすることを防ぐ効果もあります。さらに、新しいスクリーンショットを撮る際にチャートが最新の状態で表示されるよう、ChartRedraw関数でチャートを再描画します。この準備が整った時点で、新しいスクリーンショットのキャプチャが可能となります。

   ChartScreenShot(0,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT);

ここでは、ChartScreenShot関数を使ってチャートのスクリーンショットをキャプチャし、画像ファイルに保存します。この例では、0、SCREENSHOT_FILE_NAME、1366、768、ALIGN_RIGHTというパラメータを関数に渡し、スクリーンショットの撮影と保存方法を制御します。

  • 最初のパラメータ「0」は、スクリーンショットを撮りたいチャートIDを指定します。値0は現在アクティブなチャートを指します。別のチャートをキャプチャしたい場合は、特定のチャートIDを渡す必要があります。
  • 2番目のパラメータ「SCREENSHOT_FILE_NAME」は、スクリーンショットが保存されるファイル名です。私たちの場合、これは定数Our Chart ScreenShot.jpgです。このファイルは端末のFiles ディレクトリに作成され、まだ存在しない場合はスクリーンショット撮影後に生成されます。
  • 3番目と4番目のパラメータである「1366」と「768」は、スクリーンショットの寸法をピクセル単位で定義します。ここで、1366はスクリーンショットの幅、768は高さを表します。これらの値は、ユーザーの好みやキャプチャされる画面のサイズに基づいて調整することができます。
  • 最後のパラメータ「ALIGN_RIGHT」チャートウィンドウ内でのスクリーンショットの配置を指定します。ALIGN_RIGHTを指定すると、スクリーンショットがチャートの右側に配置され、必要に応じてALIGN_LEFTやALIGN_CENTERなどの他の配置オプションも利用できます。

なお、スクリーンショットの保存が遅れる場合もあるため、全てのケースに対応するために、スクリーンショットが確実に保存されるまで数秒間待機する反復処理を追加することが推奨されます。

   // Wait for 30 secs to save screenshot if not yet saved
   int wait_loops = 60;
   while(!FileIsExist(SCREENSHOT_FILE_NAME) && --wait_loops > 0){
      Sleep(500);
   }

ここでは、スクリーンショットファイルが正常に保存されるのを待つwhileループを実装し、正しい場所に正しい名前で保存されたことを確認してから続行します。待ち時間自体は十分に長いので、通常の状況であれば、スクリーンショットファイルはファイルシステム上ですぐに見つかるはずです(テスト中に保存される予定だったのであれば)。まずinteger変数wait_loopsを60に初期化します。ループの各反復でファイルが見つからずに続行すると、0.5秒(500ミリ秒(ms))の待機が発生します。つまり、ファイルが見つからなければ、ループの開始から終了までに 30秒(60反復 * 500ミリ秒)かかります。

各反復では、指定された時間内にファイルが作成されなかった場合にループが無限に実行されないように、wait_loopsカウンタもデクリメントします。さらに、sleep関数を使って、各チェックの間に500ミリ秒の遅延を設けています。これにより、頻繁に確認しすぎて、ファイルの存在要求が多すぎてシステムを圧迫するのを防ぐことができます。

最後に、その後ファイルの存在を確認する必要があります。もしファイルが存在しなければ、それ以上続ける意味がありません。アルゴリズムとロジック全体が画像ファイルに依存しているからです。もしそれが存在すれば、次のステップに進むことができます。

   if(!FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED). REVERTING NOW!");
      return (INIT_FAILED);
   }

ここでは、スクリーンショットファイルが正常に保存されたかどうかを確認するためのエラー処理メカニズムを定義します。ファイルが作成されるまでしばらく待ってから、FileIsExist関数を使ってファイルの存在を確認します。チェックがfalseを返した場合、つまりファイルが存在しない場合、「THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED).REVERTING NOW!」と表示します。このメッセージは、スクリーンショットファイルを保存できなかったことを示しています。このエラーメッセージが表示されると、プログラムは続行できなくなります。なぜなら、プログラムロジックのベースとして、その画像ファイルが完全に必要だからです。したがって、初期化が正常に完了しなかったことを示す INIT_FAILEDという戻り値で終了します。スクリーンショットが保存されていれば、そのインスタンスも知らせます。

   else if(FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE.");
   }

コードを実行すると、次のような結果が出ました。

スクリーンショットの保存

ここで、画像ファイルの最初の存在を削除し、別のものを保存することに成功したことがわかります。コンピュータ上の画像にアクセスするには、ファイル名を右クリックして、[フォルダを開く]を選択し、ファイルフォルダ内のファイルを探します。

ファイルオプション 1

あるいは、ナビゲータを開いて展開し、[ファイル]タブを右クリックして[フォルダを開く]を選択すれば、画像ファイルに直接アクセスできます。

ファイルオプション2

画像ファイルが登録されているファイルフォルダが開きます。

画像ファイルディレクトリ

ここでは、正確な画像名が登録されていることがわかります。最後に、指定された正しい情報が考慮されているかどうか、ファイルのサイズとタイプを確認してみましょう。

ファイルの種類とサイズ

ファイルの種類はJPGで、スクリーンショットの幅と高さはそれぞれ1366×768ピクセルであることがわかります。例えば、異なるファイルタイプ、例えばPNGにしたい場合、以下のようにファイルタイプだけを変更すればすみます。

   #define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.png"

このコードスニペットをコンパイルして実行すると、PNG形式の画像が生成されます。以下のプレビューでは、アニメーションを可能にするために、GIF (Graphics Interchange Format)形式で表示しています。

PNGとJPG GIF

ここまでで、チャートのスナップショットを直接ファイルに保存することに成功したことがわかります。こうして、画像ファイルをHTTPSで送信できるようにエンコードする作業は完了しました。まず、画像ファイルを開いて読み込む必要があります。

   int screenshot_Handle = INVALID_HANDLE;
   screenshot_Handle = FileOpen(SCREENSHOT_FILE_NAME,FILE_READ|FILE_BIN);
   if(screenshot_Handle == INVALID_HANDLE){
      Print("INVALID SCREENSHOT HANDLE. REVERTING NOW!");
      return(INIT_FAILED);
   }

上記のコードスニペットでは、MQL5のファイル操作に焦点を当て、以前に保存したスクリーンショットファイルを操作しています。screenshot_Handleというinteger変数を宣言し、値INVALID_HANDLEで初期化します。screenshot_Handleはファイルへの参照として機能し、INVALID_HANDLEは、有効なファイルがまだ開かれていないことを示すプレースホルダーとして機能します。この値を保持することで、ハンドルを通してファイルを参照できるようになり、何か問題が発生した場合にファイル操作から生じるエラーを処理できるようになります。

次に、FileOpen関数を使って保存したスクリーンショットを開いてみます。スクリーンショットファイルへのパスを含むスクリーンショット名を与えます。また、FILE_READFILE_BINの2つのフラグも与えています。最初のフラグは、ファイルを読みたいことをシステムに伝えます。2つ目のフラグは、おそらく2つのフラグのうちより重要なもので、ファイルがバイナリデータを含んでいることをシステムに伝えます(スクリーンショットが1と0の羅列であることと混同してはならない)。スクリーンショットは画像であり、その画像はある程度標準的な形式であるため (その形式を実際に「標準的」、「簡単」、「自然」な形式に変換すると、画像は1と0の連続になります。フォーマットも構造もなく、単なる単純な計算です。1と0の連続が異なり、画像はまったく異なって見えますが、ここではそれは問題ではありません)、何らかの形で画像に対応するバイトの連続が見つかるものと予想されます。

FileOpen関数は、ファイルを開こうとした後、有効なハンドルを返すか、INVALID_HANDLEを返します。if文でハンドルの有効性を確認します。無効なハンドルは、ファイルが正常に開かれなかったことを意味します。スクリーンショットのハンドルが無効であるというエラーメッセージを表示します。つまり、スクリーンショットが保存されていないか、アクセスできないかのどちらかです。画像ファイルを読み取れない場合は続行しても意味がないので、これ以上処理を進めず、INIT_FAILEDを返します。ハンドルIDが本当に有効であれば、成功したことをユーザーに通知します。

   else if (screenshot_Handle != INVALID_HANDLE){
      Print("SCREENSHOT WAS SAVED & OPENED SUCCESSFULLY FOR READING.");
      Print("HANDLE ID = ",screenshot_Handle,". IT IS NOW READY FOR ENCODING.");
   }

ここでは、スクリーンショットファイルが正しく開いたことを確認するために、もう1つの検証ステップを追加します。screenshot_Handleが有効である(INVALID_HANDLEと等しくない)ことを確認した後、ファイルが正しく開かれたことを示すメッセージをいくつか表示します。これは、screenshot_Handleが正常であること、そして前進する準備が整っていることを確認する別の方法です。最初のメッセージにはPrint関数を使用し、2番目のメッセージと同じこと、つまりスクリーンショットが正常に保存され、閲覧用に開かれたことを伝えます。これらの文はどちらも、ワークフローにおける現在のステップの正常な完了を確認することを目的としています。

そして、ハンドルIDを表示します。このハンドルIDは、ファイルを一意に識別し、その後の操作(読み込み、書き込み、エンコードなど)をファイルに対して実行することを許可します。ハンドルIDはデバッグにも役立ちます。これは、システムがこの特定のファイルを管理するためのリソースを取得し、割り当てたことを確認するものです。最後に、システムが次の操作を実行する準備ができたことを通知するprint文を実行します。次の操作は、スクリーンショットをエンコードし、HTTPSプロトコルを使用してネットワーク経由で送信できるようにすることです。

次に、ハンドルネームが本当に記録され保存されているか、有効な内容を持っているかを確認し、検証することができます。

   int screenshot_Handle_Size = (int)FileSize(screenshot_Handle);
   if (screenshot_Handle_Size > 0){
      Print("CHART SCREENSHOT FILE SIZE = ",screenshot_Handle_Size);
   }

ここでは、先に開いたスクリーンショットファイルのサイズをそのハンドルで取得し、確認します。スクリーンショットハンドルでFileSize関数を呼び出し、ファイルのサイズをバイト単位で返します。そして、この値を screenshot_Handle_Sizeというinteger変数に代入します。サイズが0より大きい場合、つまりファイルに何らかのデータが含まれていることを示す場合、ファイルサイズをログに出力します。このステップは、エンコードしてHTTPでファイルを送信する前に、スクリーンショットが正しく保存され、有効なコンテンツを持っていることを知らせてくれるからです。

ハンドルが本当に有効な内容を持っていれば、正しいファイルを開いていることになり、スクリーンショットファイルのバイナリデータを配列に読み込んでデコードする準備ができます。

   uchar photoArr_Data[];
   ArrayResize(photoArr_Data,screenshot_Handle_Size);
   FileReadArray(screenshot_Handle,photoArr_Data,0,screenshot_Handle_Size);
   if (ArraySize(photoArr_Data) > 0){
      Print("READ SCREENSHOT FILE DATA SIZE = ",ArraySize(photoArr_Data));
   }

まず、バイナリデータを格納するuchar配列 photoArr_Dataを宣言します。次に、ArrayResize関数を呼び出して、スクリーンショットファイルのサイズに合わせてこの配列のサイズを変更します。次に、FileReadArray関数を使って、スクリーンショットファイルの内容を「photoArr_Data配列」に、インデックス0からファイルの最後(screenshot_Handle_Size)まで読み込みます。次に、ロード後にphotoArr_Data配列のサイズを確認し、それが0より大きければ、つまり空でなければ、そのサイズをログに記録します。通常、これはスクリーンショットファイルの読み込みと処理をおこなうコードの部分であり、エンコードと送信に使用できるようにします。

ファイルの内容を読み込んで保存したら、今度は画像ファイルを閉じる必要があります。これはハンドルによっておこなわれます。

   FileClose(screenshot_Handle);

ここで、スクリーンショットファイルのデータをストレージアレイに正常に読み込んだ後、最後にスクリーンショットファイルを閉じます。FileClose関数を呼び出して、スクリーンショットファイルに関連付けられているハンドルを解放します。これにより、ファイルが開かれたときに割り当てられたシステムリソースが解放されます。ファイルへのアクセス、読み込み、書き込みなど、ファイルに対する他の操作を実行しようとする前に、ファイルが閉じられていることを確認することが極めて重要です。この関数は、ファイルへのアクセス操作がすべて完了したことを知らせるもので、スクリーンショットデータのエンコードと送信準備という次の段階に進みます。これを実行すると、次のような結果が得られます。

ファイルを読む

画像のバイナリデータが正しく読み込まれ、ストレージアレイに格納されているのがわかるでしょう。データを見るには、以下のようにArrayPrint関数を使ってログに出力すればよいです。

   ArrayPrint(photoArr_Data);

印刷すると、このようなデータになります。

画像バイナリデータ1

全データ、つまり320894までのデータを読み取り、コピーし、保存していることがわかります。

次に、写真データをBase64形式でエンコードして、HTTPで送信する準備をする必要があります。画像のようなバイナリーデータはHTTPで直接転送できないので、Base64エンコーディングを使ってバイナリーデータをASCII文字列形式に変換する必要があります。これにより、データをHTTPリクエストに安全に含めることができます。これは以下のコードスニペットで実現できます。

   //--- create boundary: (data -> base64 -> 1024 bytes -> md5)
   //Encodes the photo data into base64 format
   //This is part of preparing the data for transmission over HTTP.
   uchar base64[];
   uchar key[];
   CryptEncode(CRYPT_BASE64,photoArr_Data,key,base64);
   if (ArraySize(base64) > 0){
      Print("Transformed BASE-64 data = ",ArraySize(base64));
      //Print("The whole data is as below:");
      //ArrayPrint(base64);
   }

手始めに、2つの配列をセットアップします。最初のものはbase64です。これはエンコードされたデータを保持します。2番目の配列はkeyです。この文脈でkeyを使うことはありませんが、エンコード機能ではkeyが必要です。Base64エンコードをおこなう関数はCryptEncodeと呼ばれます。エンコーディングの種類(CRYPT_BASE64)、ソースバイナリデータ(photoArr_Data)、暗号化キー(key)、出力配列(base64)の4つのパラメータを取ります。このCryptEncode関数は、バイナリデータをBase64形式に変換し、その結果を base64配列に格納する実際の作業をおこないます。ArraySize関数でbase64のサイズを確認するとき、base64にまったく要素が含まれていなければ、つまり0より大きければ、エンコードが成功したことを意味します。

このデータを操作ログに印刷するには、ArrayPrint関数を使います。

      Print("Transformed BASE-64 data = ",ArraySize(base64));
      Print("The whole data is as below:");
      ArrayPrint(base64);

そこで以下の結果を得ます。

データ出力

元のデータバイナリサイズ320894と、新しく変換されたデータサイズ427860の間には大きな乖離があることがわかります。この乖離は、データの変換と符号化の結果です。

次に、Base64エンコードされたデータのサブセットを用意し、処理の次のステップで扱いやすい部分を確保する必要があります。具体的には、エンコードされたデータの最初の1024バイトを一時的な配列にコピーし、さらに使用することに集中する必要があります。

   //Copy the first 1024 bytes of the base64-encoded data into a temporary array
   uchar temporaryArr[1024]= {0};
   ArrayCopy(temporaryArr,base64,0,0,1024);

まず初めに、1024バイトのサイズを持つ一時配列 temporaryArrを設定します。すべての値をゼロに初期化します。この配列を使って、Base64エンコードされたデータの最初のセグメントを保持します。初期化値がゼロであるため、一時配列が格納されているメモリに残留情報が残るという潜在的な問題を避けることができます。

次に、ArrayCopy関数を使って、最初の1024バイトを base64 からtemporaryArrに移します。これは、コピー操作をクリーンかつ効率的に処理します。その理由やコピー作戦の詳細については、それぞれのストーリーがあるのだが、ほんの2、3だけ触れておきましょう。初期化の副次的な効果は、Base64エンコードされたデータの最初の部分を、ある種のランダムなちんぷんかんぷんなものとして可視化した場合の懸念を取り除くことです。空の一時配列をログに記録しましょう。これは以下のコードで実現できます。

   Print("FILLED TEMPORARY ARRAY WITH ZERO (0) IS AS BELOW:");
   ArrayPrint(temporaryArr);

コンパイルすると、このようになります。

ゼロ埋め配列

一時配列が純粋なゼロで埋められていることがわかります。これらのゼロは、元々フォーマットされていたデータの最初の1024個の値に置き換えられます。このデータもまた同様のロジックで見ることができます。

   Print("FIRST 1024 BYTES OF THE ENCODED DATA IS AS FOLLOWS:");
   ArrayPrint(temporaryArr);

記入された仮のデータ表示は以下の通りです。

フィルド一時データ

この一時データを取得した後、Base64エンコードされたデータの最初の1024バイトからMessage-Digest algorithm 5(MD5)ハッシュを生成する必要があります。このMD5ハッシュは、multipart/form-data構造の境界の一部として使用されます。これは、ファイルアップロードを処理するHTTP POSTリクエストでよく採用されます。

   //Create an MD5 hash of the temporary array
   //This hash will be used as part of the boundary in the multipart/form-data
   uchar md5[];
   CryptEncode(CRYPT_HASH_MD5,temporaryArr,key,md5);
   if (ArraySize(md5) > 0){
      Print("SIZE OF MD5 HASH OF TEMPORARY ARRAY = ",ArraySize(md5));
      Print("MD5 HASH boundary in multipart/form-data is as follows:");
      ArrayPrint(md5);
   }


手始めに、MD5ハッシュの結果を格納する md5という名前の配列を宣言します。MD5アルゴリズム(MD はMessage Digestの略)は、128ビットのハッシュ値を生成する暗号ハッシュ関数です。ハッシュは、32桁の16進数の文字列として表現されるのが最も一般的です。

この場合、CRYPT_HASH_MD5パラメータを持つMQL5内蔵関数CryptEncodeを使用してハッシュを計算します。この関数には、Base64エンコードされたデータの最初の1024バイトを保持する一時配列 temporaryArrを渡す。keyパラメータは通常、追加の暗号処理に使用されるが、MD5では不要であり、このコンテキストでは空の配列に設定されます。ハッシュ処理の結果はmd5配列に格納されます。

ハッシュを計算した後、ArraySize関数を使って配列の要素数を確認することで、md5配列が空でないことを確認します。配列に要素がある場合は、MD5ハッシュのサイズを記録し、次に実際のハッシュ値を記録します。このハッシュ値は、multipart/form-data形式の境界文字列を作成するために使用され、送信されるHTTPリクエストの異なる部分を分離するのに役立ちます。MD5アルゴリズムは、その共通性とそれが生み出すユニークな値のためにここで厳密に使用されているのであって、使用するアルゴリズムとして最良であるとか最も安全であるとかいう理由ではありません。これを実行すると、次のようなデータが得られます。

MD5データ

ここでは、MD5ハッシュデータが数字で取得されていることがわかります。したがって、MD5ハッシュを16進文字列に変換し、それをmultipart/form-data HTTPリクエストのバウンダリとして使用するための特定の長さ要件(通常は16)を満たすように切り詰める必要があります。

   //Format MD5 hash as a hexadecimal string &
   //truncate it to 16 characters to create the boundary.
   string HexaDecimal_Hash=NULL;//Used to store the hexadecimal representation of MD5 hash
   int total=ArraySize(md5);
   for(int i=0; i<total; i++){
      HexaDecimal_Hash+=StringFormat("%02X",md5[i]);
   }
   Print("Formatted MD5 Hash String is: \n",HexaDecimal_Hash);
   HexaDecimal_Hash=StringSubstr(HexaDecimal_Hash,0,16);//truncate HexaDecimal_Hash string to its first 16 characters
   //done to comply with a specific length requirement for the boundary
   //in the multipart/form-data of the HTTP request.
   Print("Final Truncated (16 characters) MD5 Hash String is: \n",HexaDecimal_Hash);

まず最初に、MD5ハッシュの16進数を格納するstring変数HexaDecimal_Hashを宣言します。この文字列は、HTTPリクエストのペイロードの異なる部分を区切る境界マーカーとなります。

次に、md5配列に格納されているハッシュの各バイトをループします。フォーマット指定子%02Xを使って、各バイトを2文字の文字列に変換します。指定子の%0の部分は、各バイトが2文字で表現されるように、必要に応じて文字列の先頭にゼロを詰めることを示します。02 は2文字(最低)、X は16進数(必要なら大文字も)であることを示します。

再び、これらの16進文字がHexaDecimal_Hash文字列に追加されます。最後に、文字列が正しく形成されたことを確認するために、文字列の内容をログに出力します。プログラムの実行結果は以下の通りです。

最終文字列ハッシュ

これは成功でした。次に、multipart/form-data HTTP POSTリクエスト用のファイルデータを作成し、準備する必要があります。このHTTP POSTリクエストは、Telegram APIを介してTelegramチャットに写真を送信するために使用されます。これには、サーバーが正しく処理できる形式で、フォームフィールドとファイルデータの両方を含むリクエストボディを準備することが含まれます。次のコードスニペットでこれを実現します。

   //--- WebRequest
   char DATA[];
   string URL = NULL;
   URL = TG_API_URL+"/bot"+botTkn+"/sendPhoto";
   //--- add chart_id
   //Append a carriage return and newline character sequence to the DATA array.
   //In the context of HTTP, \r\n is used to denote the end of a line
   //and is often required to separate different parts of an HTTP request.
   ArrayAdd(DATA,"\r\n");
   //Append a boundary marker to the DATA array.
   //Typically, the boundary marker is composed of two hyphens (--)
   //followed by a unique hash string and then a newline sequence.
   //In multipart/form-data requests, boundaries are used to separate
   //different pieces of data.
   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
   //Add a Content-Disposition header for a form-data part named chat_id.
   //The Content-Disposition header is used to indicate that the following data
   //is a form field with the name chat_id.
   ArrayAdd(DATA,"Content-Disposition: form-data; name=\"chat_id\"\r\n");
   //Again, append a newline sequence to the DATA array to end the header section
   //before the value of the chat_id is added.
   ArrayAdd(DATA,"\r\n");
   //Append the actual chat ID value to the DATA array.
   ArrayAdd(DATA,chatID);
   //Finally, Append another newline sequence to the DATA array to signify
   //the end of the chat_id form-data part.
   ArrayAdd(DATA,"\r\n");

まず、DATA配列とHTTPリクエスト用のURLを設定します。URL は、APIのベースURL(TG_API_URL)、APIに対してボットを識別するボットのトークン(botTkn)、チャットに写真を送信するためのエンドポイント(/sendPhoto)の3つの部分から構成されます。このURLは、送信する写真と写真に添付する情報である「ペイロード」を送信する「リモートサーバー」を指定します。エンドポイントURLは変更されず、私たちがおこなうすべてのリクエストに対して同じです。写真を1枚送るか数枚送るか、別々のチャットに写真を送るかなど、リクエストは同じ場所に送られます。

その後、データチャンクの端に境界マーカーを追加します。2つのハイフン(--)と独自の境界ハッシュ(HexaDecimal_Hash)で構成されます。全部では「--HexaDecimal_Hash」と表示されます。この境界マーカーは、リクエストの次の部分である「chart_id」フォームフィールドのデータチャンクの先頭に表示されます。Content-Dispositionヘッダーは、multipart/form-dataリクエストの次の部分(次のデータチャンク)がフォームフィールドであることを指定し、そのフィールド名(chart_id)を与えます。

このヘッダーと、ヘッダーセクションの終わりを示す改行文字(/r/n)を追加します。ヘッダーセクションの後、chartID 値をDATA配列に追加し、さらに改行文字(/r/n)で chart_id フォームデータ部分の終わりを示します。このプロセスは、フォームフィールドが正しくフォーマットされ、リクエストの他の部分から分離され、TelegramのAPIがデータを正しく受信して処理することを保証します。

お気づきかもしれないが、コードの中で2つの「オーバーロード関数」を使っています。コードスニペットを以下に見てみましょう。

//+------------------------------------------------------------------+
// ArrayAdd for uchar Array
void ArrayAdd(uchar &destinationArr[],const uchar &sourceArr[]){
   int sourceArr_size=ArraySize(sourceArr);//get size of source array
   if(sourceArr_size==0){
      return;//if source array is empty, exit the function
   }
   int destinationArr_size=ArraySize(destinationArr);
   //resize destination array to fit new data
   ArrayResize(destinationArr,destinationArr_size+sourceArr_size,500);
   // Copy the source array to the end of the destination array.
   ArrayCopy(destinationArr,sourceArr,destinationArr_size,0,sourceArr_size);
}

//+------------------------------------------------------------------+
// ArrayAdd for strings
void ArrayAdd(char &destinationArr[],const string text){
   int length = StringLen(text);// get the length of the input text
   if(length > 0){
      uchar sourceArr[]; //define an array to hold the UTF-8 encoded characters
      for(int i=0; i<length; i++){
         // Get the character code of the current character
         ushort character = StringGetCharacter(text,i);
         uchar array[];//define an array to hold the UTF-8 encoded character
         //Convert the character to UTF-8 & get size of the encoded character
         int total = ShortToUtf8(character,array);
         
         //Print("text @ ",i," > "text); // @ "B", IN ASCII TABLE = 66 (CHARACTER)
         //Print("character = ",character);
         //ArrayPrint(array);
         //Print("bytes = ",total) // bytes of the character
         
         int sourceArr_size = ArraySize(sourceArr);
         //Resize the source array to accommodate the new character
         ArrayResize(sourceArr,sourceArr_size+total);
         //Copy the encoded character to the source array
         ArrayCopy(sourceArr,array,sourceArr_size,0,total);
      }
      //Append the source array to the destination array
      ArrayAdd(destinationArr,sourceArr);
   }
}

ここでは、MQL5の配列へのデータの追加を処理するための2つのカスタム関数を定義します。これらの関数は、特にuchar型とstring型の両方を処理するように設計されています。これらの関数は、既存の配列に様々な情報を付加することで、HTTPリクエストデータの構築を容易にし、最終的なデータフォーマットが正しく、送信に適していることを保証します。理解しやすいように関数にコメントをつけましたが、もう一度簡単にコードの説明をしておきましょう。

最初の関数「ArrayAdd」は、符号なし文字(uchar)の配列に適用されます。ソース配列のデータをターゲット配列に追加するように設定されています。まず、ソース配列の要素数を決定します。これは、ソース配列に対して単純な関数ArraySizeを呼び出すことで達成されます。この情報をもとに、ソース配列にデータがあるかどうかを確認します。そうでない場合は、関数を早めに終了することで、続けることのばかばかしさを回避します。もしその配列にデータが含まれていれば、次のステップに進みます。そのデータを受け入れるために宛先配列のサイズを変更します。これは、出力配列に対してArrayResize関数を呼び出すことで実現するのだが、これで正しく動作することがわかったので、安心して呼び出すことができます。

もう1つの関数は、文字列をchar配列に追加するもので、次のように動作します。入力文字列の長さを計算します。入力文字列が空でない場合、文字列の各文字を取り、そのコードを取得し、それを出力配列に追加し、その過程でUTF-8に変換します。文字列を変換して出力配列に追加するために、この関数は追加される文字列データの中間格納のためにソース配列のサイズを変更します。これは、文字列のUTF-8表現と文字列そのものが、HTTPリクエストボディや他の種類のデータ構造を構築するために使われる最終的なデータ配列に正しく格納されることを保証します。

私たちが何をしたかを見るために、HTTPリクエストで送信されるチャットIDに関連する結果データを表示するロジックを実装してみましょう。

   Print("CHAT ID DATA:");
   ArrayPrint(DATA);
   string chatID_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_UTF8);
   Print("SIMPLE CHAT ID DATA IS AS FOLLOWS:",chatID_Data);   

まず始めに、ArrayPrint関数を使用して生のデータ配列を表示します。この関数は、配列の内容を出力してくれます。次に、DATA配列を文字配列から文字列形式に変換します。使用する関数はCharArrayToStringで、DATAの生のバイトデータをUTF-8エンコードされた文字列に変換します。ここで使用するパラメータは、配列全体を変換すること(WHOLE_ARRAY)と、文字エンコーディングがUTF-8 (CP_UTF8)であることを指定します。この変換が必要なのは、HTTPリクエストが文字列形式のデータを要求するからです。

結論として、最終的なフォーマットはHTTPリクエストに含まれるような文字列chatID_Dataです。Print関数を使うことで、リクエストの最終的な出力がどのようになるかを見ることができます。

チャットIDリクエスト

正しいチャットIDデータを配列に正しく追加できることがわかります。同じロジックで、画像のデータも追加して、Telegram APIにHTTP経由で写真を送信するためのmultipart/form-dataリクエストボディを構築することができます。

   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
   ArrayAdd(DATA,"Content-Disposition: form-data; name=\"photo\"; filename=\"Upload_ScreenShot.jpg\"\r\n");
   ArrayAdd(DATA,"\r\n");
   ArrayAdd(DATA,photoArr_Data);
   ArrayAdd(DATA,"\r\n");
   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"--\r\n");


まずはじめに、multipart/formデータの写真部分の境界マーカーを挿入します。これを「ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n")」という行でおこないます。2つのハイフンと「HexaDecimal_Hash」で構成される境界マーカーは、 マルチパートリクエストの異なるパートを分離する役割を果たします。HexaDecimal_Hashは、境界のためのユニークな識別子であり、リクエストの各部分が次の部分から紛れもなく分割されていることを保証するものです。

次に、フォームデータの写真部分にContent-Dispositionヘッダーを含めます。ArrayAdd関数を使って「ArrayAdd(DATA, "Content-Disposition: form-data; name=\" photo\"; filename=\"Upload_ScreenShot.jpg\"\r\n")」のように追加します。このヘッダーは、それに続くデータがファイルであること、具体的にはUpload_ScreenShot.jpgというファイルであることを示します。ヘッダーの「name=\" photo\"」の部分を通して、現在作業しているフォームデータフィールドがphotoという名前を持っていることを指定しているので、サーバーは、受信したリクエストを処理するときに、そのフィールドの一部としてUpload_ScreenShot.jpgというファイルを期待することを知っています。このファイルは単なる識別子であり、他の好きなものに変更することができます。

この後、「ArrayAdd(DATA, "\r\n")」を使用して、リクエストのヘッダーに改行シーケンスを追加します。これは、ヘッダーセクションの終わりと実際のファイルデータの始まりを示します。次に、「ArrayAdd(DATA, photoArr_Data)」を使って、実際の写真データをDATA配列に追加します。このコード行は、スクリーンショットのバイナリデータ(以前はbase64エンコードされていた)をリクエストボディに追加します。multipart/form-dataペイロードに写真データが含まれるようになりました。

最後に、「ArrayAdd(DATA, "\r\n")」で改行列をもう1つ追加し、「ArrayAdd(DATA, "--" + HexaDecimal_Hash + "--\r\n")」で写真部分を閉じる境界マーカーを追加します。境界マーカー末尾の「--」は、マルチパートセクションの終わりを示します。この最後の境界は、サーバーがリクエスト内の写真データセクションの終わりを正しく識別することを保証します。送信されたデータを見るために、前回と同様の機能を使って、ログセクションに出力してみましょう。

   Print("FINAL FULL PHOTO DATA BEING SENT:");
   ArrayPrint(DATA);
   string final_Simple_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_ACP);
   Print("FINAL FULL SIMPLE PHOTO DATA BEING SENT:",final_Simple_Data);

以下が結果です。

最終ファイルデータアップロード

最後に、Telegram APIにmultipart/form-dataリクエストを送信するために必要なHTTPリクエストヘッダを作成します。

   string HEADERS = NULL;
   HEADERS = "Content-Type: multipart/form-data; boundary="+HexaDecimal_Hash+"\r\n";

まずHEADERS文字列を定義し、「NULL」として初期化します。この文字列は、リクエストに設定するHTTPヘッダーを保持します。絶対に設定しなければならないヘッダーはContent-Typeです。Content-Typeヘッダーは、送信されるデータのタイプとそのフォーマット方法を伝えます。

正しいContent-Type値の文字列を代入します。ここで重要なのは、HEADERS文字列そのものです。HEADERS文字列へのこの特別な割り当てがなぜ必要なのかを理解するには、HTTPリクエストのフォーマットを理解しなければなりません。リクエストのフォーマットによると、リクエストはContent-Type: multipart/form-dataヘッダーを使って送信されます。ここまでできたら、あとはWebリクエストを開始するだけです。まず、以下のリクエストを送信してユーザーに知らせましょう。

   Print("SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY.");

最初のコードから、不要なWebRequestパラメータをコメントアウトし、最新のものに切り替えていきます。

   //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
   
   //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,HEADERS,10000, DATA, res, resHeaders);

ここでは、送信する新しいURL、ヘッダー、画像ファイルデータを関数に追加するだけです。応答ロジックはそのままで、以下のように変更はありません。

   // 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());
   }

プログラムを実行すると、こうなります。

MetaTrader 5:

MT5確認

Telegram:

TELEGRAMの確認

これで、MetaTrader 5取引端末からTelegramチャットに画像ファイルを送信できたことがわかります。しかし、私たちは空のスクリーンショットを送っただけです。画像ファイルにキャプションを追加するために、以下のロジックを実装します。このロジックは、オプションのキャプションをmultipart/form-dataリクエストに追加し、チャートのスクリーンショットとともにTelegram APIに送信します。

   //--- Caption
   string CAPTION = NULL;
   CAPTION = "Screenshot of Symbol: "+Symbol()+
             " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+
             ") @ Time: "+TimeToString(TimeCurrent());
   if(StringLen(CAPTION) > 0){
      ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
      ArrayAdd(DATA,"Content-Disposition: form-data; name=\"caption\"\r\n");
      ArrayAdd(DATA,"\r\n");
      ArrayAdd(DATA,CAPTION);
      ArrayAdd(DATA,"\r\n");
   }
   //---


まず、CAPTION文字列をNULLとして初期化し、次に関連する詳細で構成します。キャプションには、取引銘柄、チャートの時間枠、現在時刻が含まれ、文字列としてフォーマットされます。次に、CAPTION文字列の長さが0より大きいかどうかを確認します。もしそうなら、キャプションをDATA配列に追加します。これはマルチパートフォームデータを構築するために使用されます。これには、境界マーカーを追加し、フォームデータ部分をキャプションとして指定し、キャプションの内容そのものを含めることが含まれます。これを実行すると、次のような結果が得られます。

キャプション付き画像ファイル

うまくいきました。画像ファイルを受け取るだけでなく、銘柄名、期間、問題のチャートの時間を示す説明的なキャプションも受け取れることがわかります。

ここまでで、プログラムが添付されているチャートのスクリーンショットが得られました。別のチャートを開いて修正したい場合は、別のロジックを実装する必要があります。 

   long chart_id=ChartOpen(_Symbol,_Period);
   ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true);
   // update chart
   int wait=60;
   while(--wait>0){//decrease the value of wait by 1 before loop condition check
      if(SeriesInfoInteger(_Symbol,_Period,SERIES_SYNCHRONIZED)){
         break; // if prices up to date, terminate the loop and proceed
      }
   }

   ChartRedraw(chart_id);
   ChartSetInteger(chart_id,CHART_SHOW_GRID,false);
   ChartSetInteger(chart_id,CHART_SHOW_PERIOD_SEP,false);
   ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BEAR,clrRed);
   ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BULL,clrBlue);
   ChartSetInteger(chart_id,CHART_COLOR_BACKGROUND,clrLightSalmon);

   ChartScreenShot(chart_id,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT);
   //Sleep(10000); // sleep for 10 secs to see the opened chart
   ChartClose(chart_id);
//---

ここではまず、あらかじめ定義しておいた変数_Symbol_Period を使って、ChartOpen関数で指定した銘柄と時間枠の新しいチャートを開きます。新しいチャートのIDを変数chart_idに代入します。そして chart_idを使って、新しいチャートがメタトレーダー環境の最前面に表示され、以前のチャートに覆われていないことを確認します。

その後、最大60回反復できるループを開始します。このループの中で、チャートが同期しているかどうかを確認し続けます。同期をテストするために、パラメータに_Symbol_PeriodSERIES_SYNCHRONIZEDを指定した関数SeriesInfoIntegerを使用します。チャートが同期していることがわかれば、ループから抜け出します。チャートが同期していることを確認したら、chart_idをパラメータとする関数ChartRedrawを使ってチャートを更新します。

チャートを自分好みに調整するために、さまざまなチャート設定をカスタマイズします。ChartSetInteger関数を使って、チャートの背景色と弱気ローソク足、強気ローソク足の色を設定します。私たちが設定した色は、視覚的にわかりやすく、チャートのさまざまな要素を簡単に区別するのに役立ちます。また、グリッドとピリオド区切り文字を無効にすることで、視覚的に乱雑なチャートを少なくしています。この時点で、ご自分が適切と考えるようにチャートを修正することができます。最後に、送信用にチャートのスクリーンショットを撮ります。不必要にチャートを開いたままにしておきたくないので、スクリーンショットを撮ったらチャートを閉じます。そのためにChartClose関数を使います。プログラムを実行すると、次のような結果が得られます。

修正チャート

チャートを開き、自分好みに修正し、スナップショットを撮って最後に閉じるのは明らかです。チャートのオープンとクローズのプロセスを可視化するために、チャートを見るのに10秒遅らせてみましょう。

   Sleep(10000); // sleep for 10 secs to see the opened chart

ここでは、プログラムのバックグラウンドで何が起こっているかを見るために、チャートを10秒間開いたままにしておきます。コンパイルすると、このようになります。

チャート開閉GIF

スクリーンショットの撮影、エンコード、暗号化、取引端末からのTelegramチャットへの送信を担当するソースコードの全文は以下の通りです。

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

   //--- Get ready to take a chart screenshot of the current chart
   
   #define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.jpg"
   
   //--- First delete an instance of the screenshot file if it already exists
   if(FileIsExist(SCREENSHOT_FILE_NAME)){
      FileDelete(SCREENSHOT_FILE_NAME);
      Print("Chart Screenshot was found and deleted.");
      ChartRedraw(0);
   }
   
//---
   long chart_id=ChartOpen(_Symbol,_Period);
   ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true);
   // update chart
   int wait=60;
   while(--wait>0){//decrease the value of wait by 1 before loop condition check
      if(SeriesInfoInteger(_Symbol,_Period,SERIES_SYNCHRONIZED)){
         break; // if prices up to date, terminate the loop and proceed
      }
   }

   ChartRedraw(chart_id);
   ChartSetInteger(chart_id,CHART_SHOW_GRID,false);
   ChartSetInteger(chart_id,CHART_SHOW_PERIOD_SEP,false);
   ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BEAR,clrRed);
   ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BULL,clrBlue);
   ChartSetInteger(chart_id,CHART_COLOR_BACKGROUND,clrLightSalmon);

   ChartScreenShot(chart_id,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT);
   Print("OPENED CHART PAUSED FOR 10 SECONDS TO TAKE SCREENSHOT.")
   Sleep(10000); // sleep for 10 secs to see the opened chart
   ChartClose(chart_id);
//---
   
   //ChartScreenShot(0,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT);
   
   // Wait for 30 secs to save screenshot if not yet saved
   int wait_loops = 60;
   while(!FileIsExist(SCREENSHOT_FILE_NAME) && --wait_loops > 0){
      Sleep(500);
   }
   
   if(!FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED). REVERTING NOW!");
      return (INIT_FAILED);
   }
   else if(FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE.");
   }
   
   int screenshot_Handle = INVALID_HANDLE;
   screenshot_Handle = FileOpen(SCREENSHOT_FILE_NAME,FILE_READ|FILE_BIN);
   if(screenshot_Handle == INVALID_HANDLE){
      Print("INVALID SCREENSHOT HANDLE. REVERTING NOW!");
      return(INIT_FAILED);
   }
   
   else if (screenshot_Handle != INVALID_HANDLE){
      Print("SCREENSHOT WAS SAVED & OPENED SUCCESSFULLY FOR READING.");
      Print("HANDLE ID = ",screenshot_Handle,". IT IS NOW READY FOR ENCODING.");
   }
   
   int screenshot_Handle_Size = (int)FileSize(screenshot_Handle);
   if (screenshot_Handle_Size > 0){
      Print("CHART SCREENSHOT FILE SIZE = ",screenshot_Handle_Size);
   }
   uchar photoArr_Data[];
   ArrayResize(photoArr_Data,screenshot_Handle_Size);
   FileReadArray(screenshot_Handle,photoArr_Data,0,screenshot_Handle_Size);
   if (ArraySize(photoArr_Data) > 0){
      Print("READ SCREENSHOT FILE DATA SIZE = ",ArraySize(photoArr_Data));
   }
   FileClose(screenshot_Handle);
   
   //ArrayPrint(photoArr_Data);
   
   //--- create boundary: (data -> base64 -> 1024 bytes -> md5)
   //Encodes the photo data into base64 format
   //This is part of preparing the data for transmission over HTTP.
   uchar base64[];
   uchar key[];
   CryptEncode(CRYPT_BASE64,photoArr_Data,key,base64);
   if (ArraySize(base64) > 0){
      Print("Transformed BASE-64 data = ",ArraySize(base64));
      //Print("The whole data is as below:");
      //ArrayPrint(base64);
   }
   
   //Copy the first 1024 bytes of the base64-encoded data into a temporary array
   uchar temporaryArr[1024]= {0};
   //Print("FILLED TEMPORARY ARRAY WITH ZERO (0) IS AS BELOW:");
   //ArrayPrint(temporaryArr);
   ArrayCopy(temporaryArr,base64,0,0,1024);
   //Print("FIRST 1024 BYTES OF THE ENCODED DATA IS AS FOLLOWS:");
   //ArrayPrint(temporaryArr);
      
   //Create an MD5 hash of the temporary array
   //This hash will be used as part of the boundary in the multipart/form-data
   uchar md5[];
   CryptEncode(CRYPT_HASH_MD5,temporaryArr,key,md5);
   if (ArraySize(md5) > 0){
      Print("SIZE OF MD5 HASH OF TEMPORARY ARRAY = ",ArraySize(md5));
      Print("MD5 HASH boundary in multipart/form-data is as follows:");
      ArrayPrint(md5);
   }

   //Format MD5 hash as a hexadecimal string &
   //truncate it to 16 characters to create the boundary.
   string HexaDecimal_Hash=NULL;//Used to store the hexadecimal representation of MD5 hash
   int total=ArraySize(md5);
   for(int i=0; i<total; i++){
      HexaDecimal_Hash+=StringFormat("%02X",md5[i]);
   }
   Print("Formatted MD5 Hash String is: \n",HexaDecimal_Hash);
   HexaDecimal_Hash=StringSubstr(HexaDecimal_Hash,0,16);//truncate HexaDecimal_Hash string to its first 16 characters
   //done to comply with a specific length requirement for the boundary
   //in the multipart/form-data of the HTTP request.
   Print("Final Truncated (16 characters) MD5 Hash String is: \n",HexaDecimal_Hash);
   
   //--- WebRequest
   char DATA[];
   string URL = NULL;
   URL = TG_API_URL+"/bot"+botTkn+"/sendPhoto";
   //--- add chart_id
   //Append a carriage return and newline character sequence to the DATA array.
   //In the context of HTTP, \r\n is used to denote the end of a line
   //and is often required to separate different parts of an HTTP request.
   ArrayAdd(DATA,"\r\n");
   //Append a boundary marker to the DATA array.
   //Typically, the boundary marker is composed of two hyphens (--)
   //followed by a unique hash string and then a newline sequence.
   //In multipart/form-data requests, boundaries are used to separate
   //different pieces of data.
   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
   //Add a Content-Disposition header for a form-data part named chat_id.
   //The Content-Disposition header is used to indicate that the following data
   //is a form field with the name chat_id.
   ArrayAdd(DATA,"Content-Disposition: form-data; name=\"chat_id\"\r\n");
   //Again, append a newline sequence to the DATA array to end the header section
   //before the value of the chat_id is added.
   ArrayAdd(DATA,"\r\n");
   //Append the actual chat ID value to the DATA array.
   ArrayAdd(DATA,chatID);
   //Finally, Append another newline sequence to the DATA array to signify
   //the end of the chat_id form-data part.
   ArrayAdd(DATA,"\r\n");

   // EXAMPLE OF USING CONVERSIONS
   //uchar array[] = { 72, 101, 108, 108, 111, 0 }; // "Hello" in ASCII
   //string output = CharArrayToString(array,0,WHOLE_ARRAY,CP_ACP);
   //Print("EXAMPLE OUTPUT OF CONVERSION = ",output); // Hello
   
   Print("CHAT ID DATA:");
   ArrayPrint(DATA);
   string chatID_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_UTF8);
   Print("SIMPLE CHAT ID DATA IS AS FOLLOWS:",chatID_Data);   


   //--- Caption
   string CAPTION = NULL;
   CAPTION = "Screenshot of Symbol: "+Symbol()+
             " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+
             ") @ Time: "+TimeToString(TimeCurrent());
   if(StringLen(CAPTION) > 0){
      ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
      ArrayAdd(DATA,"Content-Disposition: form-data; name=\"caption\"\r\n");
      ArrayAdd(DATA,"\r\n");
      ArrayAdd(DATA,CAPTION);
      ArrayAdd(DATA,"\r\n");
   }
   //---
   
   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
   ArrayAdd(DATA,"Content-Disposition: form-data; name=\"photo\"; filename=\"Upload_ScreenShot.jpg\"\r\n");
   ArrayAdd(DATA,"\r\n");
   ArrayAdd(DATA,photoArr_Data);
   ArrayAdd(DATA,"\r\n");
   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"--\r\n");
   
   Print("FINAL FULL PHOTO DATA BEING SENT:");
   ArrayPrint(DATA);
   string final_Simple_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_ACP);
   Print("FINAL FULL SIMPLE PHOTO DATA BEING SENT:",final_Simple_Data);

   string HEADERS = NULL;
   HEADERS = "Content-Type: multipart/form-data; boundary="+HexaDecimal_Hash+"\r\n";
   
   Print("SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY.");
   
   //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
   
   //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,HEADERS,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
}

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


統合のテスト

EAがMetaTrader 5取引端末からTelegramに正しくスクリーンショットを送信することを確認するには、統合を徹底的にテストする必要があります。テストロジックをGIF形式で用意します。

テスティングGIF

上のGIFでは、MetaTrader 5とTelegram間のシームレスな連携によるチャートスクリーンショット送信のプロセスを示しています。このGIFは、MetaTrader 5ププラットフォームを開くところから始まり、チャートウィンドウが前景に表示された後、最終調整のために10秒間一時停止します。この間、MetaTrader 5の[操作ログ]タブには、チャートの再描画やスクリーンショットのキャプチャといった進行状況メッセージが記録されます。チャートは自動的に閉じられ、スクリーンショットがパッケージされてTelegramに送信されます。Telegram側では、チャットにスクリーンショットが表示され、統合が正常に機能していることが確認できます。このGIFにより、チャート準備からTelegramへの画像配信まで、システムがリアルタイムで自動的に動作する様子を視覚的に補足しています。


結論

この記事では、MetaTrader 5取引プラットフォームからTelegramのチャットにチャートのスクリーンショットを送信する手順をステップごとに解説しました。まず、MetaTrader 5でチャートのスクリーンショットを作成し、ChartScreenShot関数を使って画像ファイルとして保存しました。保存したファイルを開いてバイナリデータを読み込み、それをBase64形式にエンコードして、TelegramのAPIが受け取れるHTTPリクエスト形式で送信しました。これにより、チャート画像をリアルタイムでTelegramチャットに投稿することが可能になりました。

バイナリデータの送信においては、特にTelegramなどのメッセージングプラットフォーム向けの場合、HTTPプロトコルを介して直接生のバイナリデータを送ることの難しさが浮き彫りになりました。最初に理解すべきは、バイナリデータを直接送信することが不可能だという点です。Telegramをはじめとする多くのサービスでは、データをテキスト形式で送信する必要があり、ここでは広く知られたアルゴリズムを使って生のバイナリ画像データをBase64に変換することで、この要件を効率的に満たしました。その後、Base64画像をmultipart/form-data HTTPリクエストに挿入しました。このデモにより、MetaTrader 5プラットフォームの強力なカスタム自動化機能だけでなく、取引戦略に外部サービス(ここではTelegram)を統合する手法も紹介することができました。

第4回では、この記事で紹介したコードを再利用可能なコンポーネントへと発展させていきます。これにより、Telegram統合のインスタンスを複数作成し、単一の関数呼び出しに依存せず、様々なメッセージやスクリーンショットを、必要なタイミングで自由にTelegramに送信できるようになります。コードをクラス化することで、システムをよりモジュール化し、スケーラブルにし、第1回で解説した様々な取引シナリオにも容易に統合できるようにします。これにより、TelegramメカニズムをEAに統合し、ダイナミックかつ柔軟に連携させることで、複数の戦略や口座シナリオに応じて、取引中や取引終了時の重要なタイミングで様々なメッセージや画像を送信することが可能になります。この統合システムの構築と改良を引き続きおこなっていきますので、どうぞご期待ください。


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

最後のコメント | ディスカッションに移動 (18)
Aleksandr Slavskii
Aleksandr Slavskii | 9 6月 2025 において 02:38
Piotr Storozenko #:
      ::StringReplace(text, "\n", ShortToString(0x0A));
Piotr Storozenko
Piotr Storozenko | 9 6月 2025 において 11:42
Aleksandr Slavskii #:

ありがとうございます。通常のMQL5と同じ名前のパブリック関数を見つけ、名前を変えて警告を削除しました。これでコンパイルも問題なしです :)

Volker Mowy
Volker Mowy | 11 6月 2025 において 17:04
こんにちは!詳細なドキュメントをありがとうございます。残念ながら、2つのエラーメッセージが表示されます。助けていただけますか?

よろしくお願いします。

Volker Mowy
Volker Mowy | 5 7月 2025 において 13:36
Volker Mowy #:
こんにちは!詳細なドキュメントをありがとうございます。残念ながら、2つのエラーメッセージが表示されます。助けていただけますか?

よろしくお願いします。

バグが見つかりました!

666 // uchar配列のArrayAdd
667 void ArrayAdd(char &destinationArr[], const uchar &sourceArr[]){.
Volker Mowy
Volker Mowy | 6 7月 2025 において 06:33
より良いディスプレイのための小さな変化!
取引におけるナッシュ均衡ゲーム理論のHMMフィルタリングの応用 取引におけるナッシュ均衡ゲーム理論のHMMフィルタリングの応用
この記事では、ジョン・ナッシュのゲーム理論、特にナッシュ均衡の取引への応用について詳しく掘り下げます。トレーダーがPythonスクリプトとMetaTrader 5を活用し、ナッシュの原理を利用して市場の非効率性を特定し、活用する方法について解説します。また、この記事では、隠れマルコフモデル(HMM)や統計分析の利用を含むこれらの戦略を実行するためのステップバイステップのガイドを提供し、取引パフォーマンスの向上を目指します。
MQL5のパラボリックSARトレンド戦略による取引戦略の自動化:効果的なEAの作成 MQL5のパラボリックSARトレンド戦略による取引戦略の自動化:効果的なEAの作成
この記事では、MQL5を使用してパラボリックSAR戦略を基にした取引戦略を自動化する方法について説明します。効果的なエキスパートアドバイザー(EA)を創り出します。このEAは、パラボリックSAR指標によって識別されたトレンドに基づいて取引を実行します。
知っておくべきMQL5ウィザードのテクニック(第34回):非従来型RBMによる価格の埋め込み 知っておくべきMQL5ウィザードのテクニック(第34回):非従来型RBMによる価格の埋め込み
制限ボルツマンマシンは、1980年代半ば、計算資源が非常に高価だった時代に開発されたニューラルネットワークの一種です。当初は、入力された訓練データセットの次元を削減し、隠れた確率や特性を捉えるために、ギブスサンプリングとコントラストダイバージェンス(Contrastive Divergence)に依存していました。RBMが予測用の多層パーセプトロンに価格を「埋め込む」場合、バックプロパゲーションがどのように同様の性能を発揮できるかを検証します。
ダイナミックマルチペアEAの形成(第1回):通貨相関と逆相関 ダイナミックマルチペアEAの形成(第1回):通貨相関と逆相関
ダイナミックマルチペアEAは、相関戦略と逆相関戦略の両方を活用し、取引パフォーマンスの最適化を図ります。リアルタイムの市場データを分析することで、通貨ペア間の相関関係や逆相関関係を特定し、それらを取引に活かします。