ターミナル間のデータ交換にクラウドストレージサービスを使用

Dmitriy Gizlyk | 2 10月, 2017

イントロダクション

クラウド技術は、世界で人気が高まっています。 様々な有料または無料のストレージサービスを使用することができます。 しかし、実用的なトレードで使用することは可能でしょうか。 本稿では, クラウドストレージサービスを利用してターミナル間でのデータ交換を行う技術を提案します。

ターミナル間の接続のソリューションを既に持っている場合、クラウドストレージが必要ではないかもしれません。 しかし、このアプローチは、利点があると思います。 まず、プロバイダーは匿名のままです。ユーザーは、プロバイダーの PC の代わりにクラウドサーバーにアクセスできます。 したがって、プロバイダのコンピュータはウイルス攻撃から保護されており、インターネットに恒久的に接続する必要はありません。 サーバーにメッセージを送信するためにのみ接続する必要があります。 第2に、クラウドには、事実上無制限の数のプロバイダーが含まれる場合があります。また、3番目に、ユーザー数が増加しても、プロバイダはコンピューティング能力を向上させる必要がありません。

たとえば、 Googleが提供する 15 GB の無料クラウドストレージを使用してみましょう。 これは今回の目標に対し十分です。

1. 理論の概要

Google ドライブでの認証は、OAuth 2.0 プロトコルを使用しています。 サードパーティのアプリケーションや web サイトが資格情報を渡すことなく、権限のあるユーザーの保護されたリソースへのアクセスを制限できるようにするオープンな承認プロトコルです。 基本的な OAuth 2.0 アクセスシナリオは、4段階で構成されています。

  1. まず、認証 (クライアントの ID とシークレット) のデータを取得する必要があります。 このデータは、ウェブサイトによって生成され、サイトとアプリケーションに知らされます。
  2. アプリケーションが個人データにアクセスできるようにするには、アクセストークンを受け取る必要があります。 このようなトークンの1つは、' scope ' 変数で定義された異なるアクセスレベルを提供することができます。 アクセストークンがリクエストされると、アプリケーションは ' scope ' パラメータに1つ以上の値を送信できます。 このリクエストを作成するには、アプリケーションでシステムブラウザと web サービスリクエストを使用します。 一部のリクエストには、ユーザーが自分のアカウントでログインする認証手順が必要です。 ログイン後、ユーザーは、アプリケーションによってリクエストされたアクセス許可を付与する準備ができているかどうかをたずねられます。 このプロセスは、ユーザーの同意と呼ばれます。 ユーザーが同意すると、承認サーバーはアプリケーションに対して、アプリケーションがアクセストークンを取得できるようにする認証コードを提供します。 ユーザーがアクセス許可を付与しない場合、サーバーはエラーを返します。
  3. アプリケーションは、アクセストークンを受信した後、HTTP 承認ヘッダーに送信します。 アクセスポイントは、リクエストの ' scope ' パラメータに記述されている一連の操作とリソースに対してのみ有効です。 たとえば、google ドライブのアクセストークンがリリースされている場合でも、google コンタクトにアクセスすることはできません。 ただし、アプリケーションは、許可された操作を実行するために、このアクセストークンを Google ドライブに何度か送信することができます。
  4. トークンの寿命は限られています。 アクセストークンが切れた後にアプリケーションがアクセスする必要がある場合は、アプリケーションが新しいアクセストークンを受信できるようにする更新トークンを受け取ることができます。

アクセスシナリオ

2. Google ドライブへのアクセスのアレンジ

 google ドライブを使用するには、google アカウントが必要です。

アプリケーションコードを開発する前に、Google ウェブサイトで準備を行ってみましょう。 これを行うには、開発者コンソールにアクセスして、アカウントに再度ログインします。

アプリケーションの新しいプロジェクトを作成します。 プロジェクトパネル (「プロジェクトを選択」ボタンまたは ctrl + O) に移動します。 新しいプロジェクト (+) を作成します。

プロジェクトパネル

新しく開いたページで、プロジェクト名を設定し、使用条件に同意して作成を確認します。

新しいプロジェクト

パネルから新しいプロジェクトを選択し、 Google ドライブ APIを接続します。 これを行うには、マネージャーの api ライブラリで [ドライブ api ] を選択し、[有効] をクリックして新しいページで api をアクティブにします。

API ライブラリAPI のアクティベーション

新しいページでは、API を使用するための資格情報の作成を求められます。 資格情報の作成] をクリックします。

注意

Google コンソールでは、認証の種類を選択するためのウィザードが用意されていますが、必要ありません。 「クライアント ID」をクリックします。 次に、Google では、アクセス確認ページを設定する必要があることを再度注意します。 これを行うには、「同意画面の構成」をクリックします。

注意

新しく開いたページでは、すべてのフィールドをデフォルト値のままにして、"ユーザーに表示されるプロダクト名" だけをエントリーします。 次に、アプリケーションの種類を "その他" として設定し、クライアント名を指定して [作成] をクリックします。 このサービスは、"クライアント ID" と "クライアントシークレット" コードを生成します。 これをコピーすることができますが、必要ありません: json ファイルとしてをダウンロードすることができます。 「Ok」をクリックして、ローカルディスクにアクセスするためのデータを含む json ファイルをダウンロードします。

その後、サービス側の準備タスクが完了し、アプリケーションの開発を開始することができます。

3. ローカルアプリケーションと Google ドライブの間にブリッジを作成する

このタスクを解決するために、別のプログラム (ブリッジの一種) は、メタトレーダー EA またはインジケーターからのリクエストとデータを受信し、Google ドライブと接続し、メタトレーダーのアプリケーションに戻ってデータを返します。 このアプローチの利点は、まず、google が C# で google ドライブを操作するためのライブラリを提供していることです。 これは開発を非常に楽にします。 第2に、サードパーティ製アプリケーションを使用すると、外部サービスとの間で「リソースの消費」の exchange 操作からターミナルが保存されます。 第3に、この解除はプラットフォームからアプリケーションを作成し、MetaTrader4と MetaTrader5 アプリケーションの両方で動作する関数を備えたクロスプラットフォームになります。

先ほども述べたように、ブリッジアプリケーションは、Google ライブラリを使用して C# で開発される予定です。 VisualStudio で Windows フォームプロジェクトを作成し、NuGet を使用してGoogle api. ドライブ v3ライブラリを追加してみましょう。

次に、Google ドライブを操作するためのGoogleDriveClassクラスを作成してみましょう。

class GoogleDriveClass
    {
        static string[] Scopes = { DriveService.Scope.DriveFile };  //クラスを操作するための配列
        static string ApplicationName = "Google Drive Bridge";      //アプリケーション名
        public static UserCredential credential = null;             //承認キー
        public static string extension = ".gdb";                    //保存されたファイルの拡張子
    }

まず、サービスにログインするための関数を作成してみましょう。 以前保存した json ファイルをアクセスコードで適用することです。 この場合は、「クライアント・シークレット・ json」です。 ファイルを別の名前で保存した場合は、関数コードで指定します。 ログをデータに読み込んだ後、サービスの非同期承認関数が呼び出されます。 ログインが成功した場合は、後でアクセスできるように、 token資格情報オブジェクトに保存されます。C# でする場合、例外処理については忘れないでください。例外が発生した場合は、資格情報オブジェクトがリセットされます。 

        public bool Authorize()
        {
            using (System.IO.FileStream stream =
                     new System.IO.FileStream("client-secret.json", System.IO.FileMode.Open, System.IO.FileAccess.Read))
            {
                try
                {
                    string credPath = System.Environment.CurrentDirectory.ToString();
                    credPath = System.IO.Path.Combine(credPath, "drive-bridge.json");

                    credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                        GoogleClientSecrets.Load(stream).Secrets,
                        GoogleDriveClass.Scopes,
                        "example.bridge@gmail.com",
                        CancellationToken.None,
                        new FileDataStore(credPath, true)).Result;
                }
                catch (Exception)
                {
                    credential = null;
                }

            }
            return (credential != null);
        }

Google ドライブを使用する場合、「ブリッジ」では、ディスクへのデータの書き込みと必要なファイルの読み取りという2つの関数を実行する必要があります。 より詳細に考慮してみましょう。 こうしたシンプルな関数を実装するには、手順を記述する必要があります。 その理由は、Google ドライブのファイルシステムは、今までの慣れているものとは異なります。 ここでは、名前とファイル拡張子は、慣習的なプレゼンテーションを維持するために別々のエントリとして存在します。 実際には、保存時に、各ファイルには、その下に格納されている一意の ID が割り当てられます。 したがって、ユーザーは、同じ名前と拡張子を持つファイルの数を無制限に保存することができます。 ファイルにアクセスする前に、そのクラウドストア ID を知る必要があります。 これを行うには、ディスク上のすべてのファイルの一覧を読み込んで、指定した名前を1つずつ比較します。

GetFileList関数は、ファイルリストの取得を担当します。 Google.Apis.Drive.v3.Data.Filクラスリストを返します。 以前ダウンロードしたライブラリのGoogle.Apis.Drive.v3.DriveServiceクラスを使用して、google ドライブからファイルリストを受信してみましょう。 クラスの初期化時には、プロジェクト名と一緒にログインするときに取得したトークンを渡します。 結果のリストは、返されたresult変数に格納されます。 例外が発生した場合、変数はゼロにリセットされます。 ファイルリストはリクエストされ、他の関数と同時に処理した。

using File = Google.Apis.Drive.v3.Data.File;
        public IList<File> GetFileList()
        {
            IList<File> result = null;
            if (credential == null)
                this.Authorize();
            if (credential == null)
            {
                return result;
            }
            //ドライブ API サービスを作成。
            using (Google.Apis.Drive.v3.DriveService service = new Google.Apis.Drive.v3.DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            }))
            {
                try
                {
                    //リクエストのパラメータを定義します。
                    FilesResource.ListRequest listRequest = service.Files.List();
                    listRequest.PageSize = 1000;
                    listRequest.Fields = "nextPageToken, files(id, name, size)";

                    //リストファイル。
                    result = listRequest.Execute().Files;
                }
                catch (Exception)
                {
                    return null;
                }
            }
            return result;
        }


3.1. クラウドストレージへのデータの書き込み

クラウドストレージにファイルを書き込むFileCreate関数を作成します。 この関数のインプットパラメータは、ファイル名とその内容です。 正常に作成された場合には、ディスク上の操作結果とファイル IDを返します。 すでに使い慣れたGoogle.Apis.Drive.v3.DriveServicクラスは、ファイルの作成を担当し、一方、 >Google.Apis.Drive.v3.FilesResource.CreateMediaUploadクラスは、リクエストを送信するために使用されます。 ファイルパラメータでは、シンプルなテキストファイルであることを示し、コピーする権限を与えます。

       public bool FileCreate(string name, string value, out string id)
        {
            bool result = false;
            id = null;
            if (credential == null)
                this.Authorize();
            if (credential == null)
            {
                return result;
            }
            using (var service = new Google.Apis.Drive.v3.DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            }))

            {
                var body = new File();
                body.Name = name;
                body.MimeType = "text/json";
                body.ViewersCanCopyContent = true;

                byte[] byteArray = Encoding.Default.GetBytes(value);
                using (var stream = new System.IO.MemoryStream(byteArray))
                {
                    Google.Apis.Drive.v3.FilesResource.CreateMediaUpload request = service.Files.Create(body, stream, body.MimeType);
                    if (request.Upload().Exception == null)
                    { id = request.ResponseBody.Id; result = true; }
                }
            }
            return result;
        }

ファイルを作成した後の次の手順は、更新の関数です。 アプリケーションの目的と Google ドライブファイルシステムの関数を思い出してみましょう。 異なる pc 上にある複数のターミナル間でデータを交換するためのアプリケーションを開発しています。 多くのターミナルに情報が必要です。 しかし、クラウドファイルシステムの関数は、同じ名前と拡張子を持つファイルを作成することができます。 最初に新しいデータを含む新しいファイルを作成し、その後、クラウドストレージから古いデータを削除できます。 これが、 FileUpdate関数が行うことです。 そのインプットパラメータはファイルの名前とその内容であり、演算結果の論理値を返します。

関数の先頭で、 new_idテキスト変数を宣言し、以前に作成したFileCreate関数を呼び出して、新しいデータファイルをクラウドに作成し、新しいファイル id を変数に返します。

次に、 GetFileList関数からクラウド内のすべてのファイルの一覧を取得し、新しく作成されたファイルの名前と ID を1つずつ比較します。 不要な重複はすべて、ストレージから削除されます。 ここでは、既に既知のGoogle.Apis.Drive.v3.DriveServiceクラスを使用していますが、リクエストはGoogle.Apis.Drive.v3.FilesResource.DeleteRequestクラスを使用して送信されます。

        public bool FileUpdate(string name, string value)
        {
            bool result = false;
            if (credential == null)
                this.Authorize();
            if (credential == null)
            {
                return result;
            }

            string new_id;
            if (FileCreate(name, value, out new_id))
            {
                IList<File> files = GetFileList();
                if (files != null && files.Count > 0)
                {
                    result = true;
                    try
                    {
                        using (var service = new DriveService(new BaseClientService.Initializer()
                        {
                            HttpClientInitializer = credential,
                            ApplicationName = ApplicationName,
                        }))
                        {
                            foreach (var file in files)
                            {
                                if (file.Name == name && file.Id != new_id)
                                {
                                    try
                                    {
                                        Google.Apis.Drive.v3.FilesResource.DeleteRequest request = service.Files.Delete(file.Id);
                                        string res = request.Execute();
                                    }
                                    catch (Exception)
                                    {
                                        continue;
                                    }
                                }
                            }
                        }
                    }
                    catch (Exception)
                    {
                        return result;
                    }
                }
            }
            return result;
        }

3.2. クラウドストレージからのデータの読み取り

クラウドストレージにデータを書き込む関数を既に作成しています。 今はそれを読み返すための時間です。 ファイルをダウンロードする前に、クラウド内の ID を取得する必要があります。 これが GetFileID関数の目的です。 インプットパラメータは、必要なファイルの名前で、戻り値は ID です。 この関数の論理構造はシンプルです。GetFileList関数からファイルのリストを受け取り、ファイルを並べ替えるだけで、必要な名前の最初のファイルを探します。 ほとんどの場合、最も古いファイルになります。 この時点で、必要なパラメータを持つ新しいファイルが保存されるか、またはダウンロード中にエラーが発生するというリスクがあります。 完全なデータを取得するために、リスクを受け入れてみましょう。 最新の変更は、次回の更新時にダウンロードされます。 新しいデータファイルが作成された後、不要な重複はすべてFileUpdate関数から削除されます。

        public string GetFileId(string name)
        {
            string result = null;
            IList<File> files = GetFileList();

            if (files != null && files.Count > 0)
            {
                foreach (var file in files)
                {
                    if (file.Name == name)
                    {
                        result = file.Id;
                        break;
                    }
                }
            }
            return result;
        }

ファイル ID を取得した後、必要なデータを取得することができます。 これを行うには、必要なファイル ID が渡されるFileRead関数が必要ですが、この関数はその内容を返します。 失敗した場合、関数は空の文字列を返します。 以前のように、 Google.Apis.Drive.v3.DriveServiceクラスの接続を作成し、 Google.Apis.Drive.v3.FilesResource.GetRequestクラスのリクエストを作成する必要があります。

        public string FileRead(string id)
        {
            if (String.IsNullOrEmpty(id))
            {
                return ("Errore. File not found");
            }
            bool result = false;
            string value = null;
            if (credential == null)
                this.Authorize();
            if (credential != null)
            {
                using (var service = new DriveService(new BaseClientService.Initializer()
                {
                    HttpClientInitializer = credential,
                    ApplicationName = ApplicationName,
                }))
                {
                    Google.Apis.Drive.v3.FilesResource.GetRequest request = service.Files.Get(id);
                    using (var stream = new MemoryStream())
                    {
                        request.MediaDownloader.ProgressChanged += (IDownloadProgress progress) =>
                        {
                            if (progress.Status == DownloadStatus.Completed)
                                result = true;
                        };
                        request.Download(stream);

                        if (result)
                        {
                            int start = 0;
                            int count = (int)stream.Length;
                            value = Encoding.Default.GetString(stream.GetBuffer(), start, count);
                        }
                    }
                }
            }
            return value;
        }

3.3. ターミナルアプリケーションとの相互作用ブロックの作成

Google ドライブクラウドストレージにアプリケーションを接続したので、MetaTrader アプリケーションにも接続することができるようになりました。 結局のところ、これが主な目的です。 名前付きパイプを使用してこの接続を確立することを決めました。 操作は web サイトで既に説明されており、MQL5 言語は既にこの接続タイプを使用するためのCFilePipeクラスを備えています。 これでアプリケーションを作成するときにタスクが容易になります。

このターミナルは、複数のアプリケーションを起動することができます。 したがって、 "ブリッジ" は、同時に接続を処理することができるはずです。 その非同期マルチスレッドプログラミングモデルを使用してみましょう。

ブリッジとアプリケーションの間で送信されるメッセージの形式を定義する必要があります。 クラウドからファイルを読み取るには、コマンドとファイル名を渡す必要があります。 クラウドにファイルを書き込むには、コマンド、ファイル名、およびその内容を送信する必要があります。 このデータはパイプ内の単一のスレッドとして送信されるため、情報全体を1つの文字列に渡すのが妥当です。 文字列内のフィールドの区切り文字として ";" を適用します。

まず、グローバル変数を宣言してみましょう。

        GoogleDriveClass Drive = new GoogleDriveClass();
        private static int numThreads = 10;
        private static string pipeName = "GoogleBridge";
        static Thread[] servers;

PipesCreate操作スレッドを起動するための関数を作成します。 この関数では、スレッドの配列を初期化し、ループ内で起動します。 各スレッドを起動すると、 ServerThread関数が呼び出され、スレッド内の関数が初期化されます。

        public void PipesCreate()
        {
            int i;
            servers = new Thread[numThreads];

            for (i = 0; i < numThreads; i++)
            {
                servers[i] = new Thread(ServerThread);
                servers[i].Start();
            }
        }

また、名前付きパイプが作成され、パイプへのクライアント接続を待機する非同期関数が各スレッドの開始時に起動されます。クライアントをパイプに接続すると、Connected関数が呼び出されます。 これを実現するには、 AsyncCallback asyn_connectedデリゲートを作成します。例外が発生した場合、スレッドは再起動されます。

        private void ServerThread()
        {
            NamedPipeServerStream pipeServer =
                new NamedPipeServerStream(pipeName, PipeDirection.InOut, numThreads, PipeTransmissionMode.Message, PipeOptions.Asynchronous);

            int threadId = Thread.CurrentThread.ManagedThreadId;
            //クライアントが接続するのを待つ
            AsyncCallback asyn_connected = new AsyncCallback(Connected);
            try
            {
                pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
            }
            catch (Exception)
            {
                servers[threadId].Suspend();
                servers[threadId].Start();
            }
        }

クライアントが名前付きパイプに接続すると、パイプの状態をチェックし、例外が発生した場合はスレッドを再起動します。 接続が安定している場合は、アプリケーションからのリクエストを読み取る関数を開始します。 読み取り関数がfalseを返す場合は、接続を再起動します。

        private void Connected(IAsyncResult pipe)
        {
            if (!pipe.IsCompleted)
                return;
            bool exit = false;
            try
            {
                NamedPipeServerStream pipeServer = (NamedPipeServerStream)pipe.AsyncState;
                try
                {
                    if (!pipeServer.IsConnected)
                        pipeServer.WaitForConnection();
                }
                catch (IOException)
                {
                    AsyncCallback asyn_connected = new AsyncCallback(Connected);
                    pipeServer.Dispose();
                    pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, numThreads, PipeTransmissionMode.Message, PipeOptions.Asynchronous);
                    pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
                    return;
                }
                while (!exit && pipeServer.IsConnected)
                {
                    //クライアントからのリクエストを読み取ります。 Once the client has
                    //パイプに書き込まれたセキュリティトークンが使用可能になります。

                    while (pipeServer.IsConnected)
                    {
                        if (!ReadMessage(pipeServer))
                        {
                            exit = true;
                            break;
                        }
                    }
                    //クライアントが接続するのを待つ
                    AsyncCallback asyn_connected = new AsyncCallback(Connected);
                    pipeServer.Disconnect();
                    pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
                    break;
                }
            }
            finally
            {
                exit = true;
            }
        }

ReadMessage関数は、アプリケーションからのリクエストを読み取り、処理します。 スレッドオブジェクトへの参照は、パラメータとして関数に渡されます。 この関数の結果は、操作の論理値です。 まず、この関数は、名前付きパイプからアプリケーションリクエストを読み取り、フィールドに分割します。 次に、コマンドを認識し、必要なアクションを実行します。

この関数は3つのコマンドを備えています:

現在の接続を閉じるには、この関数はfalseを返す必要があります。 呼び出された接続関数は、すべての残りの部分を行います。

ファイルの読み取りリクエストを実行するには、ファイル ID を定義し、上記のGetFileIDおよびFileRead関数を使用してその内容を読み取る必要があります。

ファイル書き込み関数を実行した後、以前に作成したFileUpdate関数を呼び出します。

もちろん、例外処理については忘れないでください。 例外が発生した場合は、Google に再度ログインしてください。

        private bool ReadMessage(PipeStream pipe)
        {
            if (!pipe.IsConnected)
                return false;

            byte[] arr_read = new byte[1024];
            string message = null;
            int length;
            do
            {
                length = pipe.Read(arr_read, 0, 1024);
                if (length > 0)
                    message += Encoding.Default.GetString(arr_read, 0, length);
            } while (length >= 1024 && pipe.IsConnected);
            if (message == null)
                return true;

            if (message.Trim() == "Close\0")
                return false;

            string result = null;
            string[] separates = { ";" };
            string[] arr_message = message.Split(separates, StringSplitOptions.RemoveEmptyEntries);
            if (arr_message[0].Trim() == "Read")
            {
                try
                {
                    result = Drive.FileRead(Drive.GetFileId(arr_message[1].Trim() + GoogleDriveClass.extension));
                }
                catch (Exception e)
                {
                    result = "Error " + e.ToString();
                    Drive.Authorize();
                }
                return WriteMessage(pipe, result);
            }

            if (arr_message[0].Trim() == "Write")
            {
                try
                {
                    result = (Drive.FileUpdate(arr_message[1].Trim() + GoogleDriveClass.extension, arr_message[2].Trim()) ? "Ok" : "Error");
                }
                catch (Exception e)
                {
                    result = "Error " + e.ToString();
                    Drive.Authorize();
                }

                return WriteMessage(pipe, result);
            }
            return true;
        }

リクエストを処理した後、操作の結果をアプリケーションに返す必要があります。 WriteMessage関数を作成してみましょう。 このパラメータは、現在の名前付きパイプのオブジェクトへの参照であり、アプリケーションに送信されるメッセージです。 この関数は、演算結果に関する論理値を返します。

        private bool WriteMessage(PipeStream pipe, string message)
        {
            if (!pipe.IsConnected)
                return false;
            if (message == null || message.Count() == 0)
                message = "Empty";
            byte[] arr_bytes = Encoding.Default.GetBytes(message);
            try
            {
                pipe.Flush();
                pipe.Write(arr_bytes, 0, arr_bytes.Count());
                pipe.Flush();
            }
            catch (IOException)
            {
                return false;
            }
            return true;
        }

必要なすべての関数を説明したので、 PipesCreate関数を実行します。 Windows フォームプロジェクトを作成したので、Form1 関数からこの関数を実行します。

        public Form1()
        {
            InitializeComponent();
            PipesCreate();
        }

プロジェクトを再コンパイルし、クラウドストレージアクセスデータを使用して json ファイルをアプリケーションフォルダにコピーするだけです。
  

4. メタトレーダーアプリケーションの作成

プログラムの実用的なアプリケーション意識に向けます。 まず、シンプルなグラフィカルオブジェクトをコピーするためのプログラムを作成することをお勧めします。

4.1. グラフィカルオブジェクトを操作するためのクラス

別のチャートで作成するために、どのオブジェクトデータを渡す必要があるでしょうか。 おそらく、識別のオブジェクトの種類とその名前である必要があります。 また、オブジェクトの色とその座標も必要になります。 最初の質問は、どれだけ多くの座標を使うかによります。 たとえば、垂直線にデータを渡す場合は、日付を渡すだけで十分です。 水平線を扱う場合は、価格を渡す必要があります。 トレンドラインでは、日付、価格、線の方向 (右/左) の2組の座標が必要です。 異なるオブジェクトには、共通パラメータと一意パラメータの両方があります。 ただし、MQL5 では、ObjectCreate、ObjectSetInteger、ObjectSetDouble、および ObjectSetString の4つの関数を使用して、すべてのオブジェクトが作成および変更されます。 同じパスに従って、パラメータの型、プロパティと値を渡します。

パラメータ型の列挙体を作成してみましょう。

enum ENUM_SET_TYPE
  {
   ENUM_SET_TYPE_INTEGER=0,
   ENUM_SET_TYPE_DOUBLE=1,
   ENUM_SET_TYPE_STRING=2
  };

オブジェクトデータを処理するためのCCopyObjectクラスを作成します。 初期化中に文字列パラメータが渡されます。 続いて、チャート上のクラスによって作成されたオブジェクトを識別します。 この値は、s_ObjectsID クラス変数に保存されます。

class CCopyObject
  {
private:
   string            s_ObjectsID;

public:
                     CCopyObject(string objectsID="CopyObjects");
                    ~CCopyObject();
  };
//+------------------------------------------------------------------+
//                                                                 |
//+------------------------------------------------------------------+
CCopyObject::CCopyObject(string objectsID="CopyObjects")
  {
   s_ObjectsID = (objectsID==NULL || objectsID=="" ? "CopyObjects" : objectsID);
  }

4.1.1. オブジェクトデータを収集するための関数のブロック

CreateMessage関数を作成します。 このパラメータは、必要なチャートの ID です。 この関数は、オブジェクトパラメータとその値のリストを含むクラウドストレージに送信されるテキスト値を返します。 返される文字列は、このデータを読み取ることができるように構造化する必要があります。 各オブジェクトのデータが中括弧に入れ、"|" シンボルは、パラメータと "=" シンボルの間の区切りシンボルとして使用され、パラメータとその値を分離します。 各オブジェクトの説明の先頭に、その名前と型が示され、その型に対応するオブジェクト記述関数がその後に呼び出されます。

string CCopyObject::CreateMessage(long chart)
  {
   string result = NULL;
   int total = ObjectsTotal(chart, 0);
   for(int i=0;i<total;i++)
     {
      string name = ObjectName(chart, i, 0);
      switch((ENUM_OBJECT)ObjectGetInteger(chart,name,OBJPROP_TYPE))
        {
         case OBJ_HLINE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_HLINE)+"|"+HLineToString(chart, name)+"}";
           break;
         case OBJ_VLINE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_VLINE)+"|"+VLineToString(chart, name)+"}";
           break;
         case OBJ_TREND:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_TREND)+"|"+TrendToString(chart, name)+"}";
           break;
         case OBJ_RECTANGLE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_RECTANGLE)+"|"+RectangleToString(chart, name)+"}";
           break;
        }
     }
   return result;
  }

たとえば、 HLineToString関数は、水平線を記述するために呼び出されます。 チャート ID とオブジェクト名は、パラメータとして使用されます。 この関数は、オブジェクトパラメータを使用して構造化文字列を返すことです。 たとえば、水平線の場合、渡されたパラメータは、価格、色、線幅、および線がチャートの前面または背景に表示されるかどうかです。 パラメータプロパティの前に、以前に作成した列挙体からパラメータの型を設定することを忘れないでください。

string CCopyObject::HLineToString(long chart,string name)
  {
   string result = NULL;
   if(ObjectFind(chart,name)!=0)
      return result;
   
   result+=IntegerToString(ENUM_SET_TYPE_DOUBLE)+"="+IntegerToString(OBJPROP_PRICE)+"=0="+DoubleToString(ObjectGetDouble(chart,name,OBJPROP_PRICE,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_COLOR)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_COLOR,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_STYLE)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_STYLE,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_BACK)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_BACK,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_WIDTH)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_WIDTH,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TEXT)+"=0="+ObjectGetString(chart,name,OBJPROP_TEXT,0)+"|";
   result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TOOLTIP)+"=0="+ObjectGetString(chart,name,OBJPROP_TOOLTIP,0);
   return result;
  }

同様に、他のオブジェクトタイプを記述するための関数を作成します。 今回の場合、垂直線のVLineToString 、トレンドラインのTrendToStringと長方形のRectangleToStringです。 この関数のコードは、添付されたクラスコードにあります。

4.1.2. チャート上のオブジェクトをプロットするための関数

データ収集の関数を作成しました。 ここでは、メッセージを読み込んで、チャート上のオブジェクトをプロットする関数: DrawObjectsを開発してみましょう。 パラメータは、チャート ID と受信メッセージです。 この関数は、操作の実行の論理値を返します。

この関数のアルゴリズムには、段階があります。

bool CCopyObject::DrawObjects(long chart,string message)
  {
   //---オブジェクトにメッセージを分割
   StringTrimLeft(message);
   StringTrimRight(message);
   if(message==NULL || StringLen(message)<=0)
      return false;
   StringReplace(message,"{","");
   string objects[];
   if(StringSplit(message,'}',objects)<=0)
      return false;
   int total=ArraySize(objects);
   SObject Objects[];
   if(ArrayResize(Objects,total)<0)
      return false;
  
   //---設定にすべてのオブジェクトのメッセージを分割
   for(int i=0;i<total;i++)
     {
      string settings[];
      int total_settings=StringSplit(objects[i],'|',settings);
      //---検索名とオブジェクトのタイプ
      int set=0;
      while(set<total_settings && Objects[i].name==NULL && Objects[i].type==-1)
        {
         string param[];
         if(StringSplit(settings[set],'=',param)<=1)
           {
            set++;
            continue;
           }
         string temp=param[0];
         StringTrimLeft(temp);
         StringTrimRight(temp);
         if(temp=="NAME")
           {
            Objects[i].name=param[1];
            StringTrimLeft(Objects[i].name);
            StringTrimRight(Objects[i].name);
            Objects[i].name=s_ObjectsID+Objects[i].name;
           }
         if(temp=="TYPE")
            Objects[i].type=(int)StringToInteger(param[1]);
         set++;
        }
      //---オブジェクトの名前または型が見つからない場合は、次のオブジェクトに移動します
      if(Objects[i].name==NULL || Objects[i].type==-1)
         continue;
      //---チャート上のオブジェクトを検索
      int subwindow=ObjectFind(chart,Objects[i].name);
      //---オブジェクトがチャットで見つかったが、メインサブまたはそのタイプではない場合、チャートからこの oject を削除する
      if(subwindow>0 || (subwindow==0 && ObjectGetInteger(chart,Objects[i].name,OBJPROP_TYPE)!=Objects[i].type))
        {
         if(!ObjectDelete(chart,Objects[i].name))
            continue;
         subwindow=-1;
        }
      //---オブジェクトが見つからない場合は、チャートに作成します。
      if(subwindow<0)
        {
         if(!ObjectCreate(chart,Objects[i].name,(ENUM_OBJECT)Objects[i].type,0,0,0))
            continue;
         ObjectSetInteger(chart,Objects[i].name,OBJPROP_HIDDEN,true);
         ObjectSetInteger(chart,Objects[i].name,OBJPROP_SELECTABLE,false);
         ObjectSetInteger(chart,Objects[i].name,OBJPROP_SELECTED,false);
        }      
      //---
      CopySettingsToObject(chart,Objects[i].name,settings);
     }
   //---
   DeleteExtraObjects(chart,Objects);
   return true;
  }

メッセージで受信したプロパティを chart オブジェクトに割り当てる関数は、汎用的であり、任意のオブジェクトタイプに適用できます。 チャート ID、オブジェクト名、およびパラメータの文字列配列は、パラメータとして渡されます。 各配列要素は、操作の種類、プロパティ、修飾子、および値に分割されます。 取得した値は、操作の種類に対応する関数を通じてオブジェクトに割り当てられます。 

bool CCopyObject::CopySettingsToObject(long chart,string name,string &settings[])
  {
   int total_settings=ArraySize(settings);
   if(total_settings<=0)
      return false;
   
   for(int i=0;i<total_settings;i++)
     {
      string setting[];
      int total=StringSplit(settings[i],'=',setting);
      if(total<3)
         continue;
      switch((ENUM_SET_TYPE)StringToInteger(setting[0]))
        {
         case ENUM_SET_TYPE_INTEGER:
           ObjectSetInteger(chart,name,(ENUM_OBJECT_PROPERTY_INTEGER)StringToInteger(setting[1]),(int)(total==3 ? 0 : StringToInteger(setting[2])),StringToInteger(setting[total-1]));
           break;
         case ENUM_SET_TYPE_DOUBLE:
           ObjectSetDouble(chart,name,(ENUM_OBJECT_PROPERTY_DOUBLE)StringToInteger(setting[1]),(int)(total==3 ? 0 : StringToInteger(setting[2])),StringToDouble(setting[total-1]));
           break;
         case ENUM_SET_TYPE_STRING:
           ObjectSetString(chart,name,(ENUM_OBJECT_PROPERTY_STRING)StringToInteger(setting[1]),(int)(total==3 ? 0 : StringToInteger(setting[2])),setting[total-1]);
           break;
        }
     }
   return true;
  }

チャート上のオブジェクトをプロットした後、チャート上に存在するオブジェクトをメッセージに渡されたものと比較する必要があります。 必要な ID が含まれているが、メッセージに存在しない「不要」なオブジェクトは、チャートから削除されます (プロバイダーによって削除されたオブジェクトです)。 DeleteExtraObjects 関数は、その役割を担います。 パラメータは、チャート ID と、オブジェクト名と型を含む構造体の配列です。

void CCopyObject::DeleteExtraObjects(long chart,SObject &Objects[])
  {
   int total=ArraySize(Objects);
   for(int i=0;i<ObjectsTotal(chart,0);i++)
     {
      string name=ObjectName(chart,i,0);
      if(StringFind(name,s_ObjectsID)!=0)
         continue;
      bool found=false;
      for(int obj=0;(obj<total && !found);obj++)
        {
         if(name==Objects[obj].name && ObjectGetInteger(chart,name,OBJPROP_TYPE)==Objects[obj].type)
           {
            found=true;
            break;
           }
        }
      if(!found)
        {
         if(ObjectDelete(chart,name))
            i--;
        }
     }
   return;
  }

4.2. プロバイダアプリケーション

徐々に結論に近づいています。 オブジェクトデータを収集し、クラウドストレージに送信するプロバイダーアプリケーションを作成してみましょう。 EAという形で実行してみましょう。 外部パラメータは1つだけです: アプリケーションをターミナルにダウンロードした直後にデータを送信するかどうかを定義するSendAtStart論理変数。

sinput bool       SendAtStart =  true; //nit でメッセージを送信する

アプリケーションヘッダーに必要なライブラリを含めます。 上記で説明したグラフィカルオブジェクトを使用するクラスと、名前付きパイプを使用するための基本クラスです。 また、アプリケーションが接続されているパイプ名を指定します。

#include<CopyObject.mqh>
#include<Files\FilePipe.mqh>

#define                     Connection       "\\\\.\\pipe\\GoogleBridge"

グローバル変数で、グラフィカルオブジェクトを使用するためのクラスを宣言し、最後に送信されたメッセージを保存するための文字列変数と uchar 配列クラウドストレージへの接続を閉じるためのコマンドが書き込まれます。

CCopyObject *CopyObjects;
string PrevMessage;
uchar Close[];

このOnInit関数で、グローバル変数を初期化し、必要に応じてクラウドストレージにデータを送信するための関数を起動します。 

int()
  {
//---
   CopyObjects = new CCopyObject();
   PrevMessage="Init";
   StringToCharArray(("Close"),Close,0,WHOLE_ARRAY,CP_UTF8);
   if(SendAtStart)
      SendMessage(ChartID());
//---
   return(INIT_SUCCEEDED);
  }

OnDeinit 関数で、グラフィカルオブジェクトを操作するためのオブジェクトクラスを削除します。

void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(CopyObjects)!=POINTER_INVALID)
      delete CopyObjects;
  }

情報メッセージをクラウドストレージに送信するための関数は、オブジェクトが作成、変更、またはチャートから削除されたときに、 OnChartEvent関数から呼び出されます。

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   int count=10;
   switch(id)
     {
      case CHARTEVENT_OBJECT_CREATE:
      case CHARTEVENT_OBJECT_DELETE:
      case CHARTEVENT_OBJECT_CHANGE:
      case CHARTEVENT_OBJECT_DRAG:
      case CHARTEVENT_OBJECT_ENDEDIT:
        while(!SendMessage(ChartID()) && !IsStopped() && count>=0)
           {
            count--;
            Sleep(500);
           }
        break;
     }      
  }

メイン操作は、チャート ID をエントリーとして適用するSendMessage関数で実行されます。 そのアルゴリズムは、いくつかの段階に分けることができます:

操作の実行中に、チャートへのコメントに情報メッセージを表示します。
bool SendMessage(long chart)
  {
   Comment("Sending message");
   if(CheckPointer(CopyObjects)==POINTER_INVALID)
     {
      CopyObjects = new CCopyObject();
      if(CheckPointer(CopyObjects)==POINTER_INVALID)
         return false;
     }
   string message=CopyObjects.CreateMessage(chart);
   if(message==NULL || PrevMessage==message)
      return true;
   
   string Name=SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_BASE)+SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_PROFIT);
   CFilePipe *pipe=new CFilePipe();
   int handle=pipe.Open(Connection,FILE_WRITE|FILE_READ);
   if(handle<=0)
     {
      Comment("Pipe doesn't found");
      delete pipe;
      return false;
     }
   uchar iBuffer[];
   int size=StringToCharArray(("Write;"+Name+";"+message),iBuffer,0,WHOLE_ARRAY,CP_UTF8);
   if(pipe.WriteArray(iBuffer)<=0)
     {
      Comment("Error of sending request");
      pipe.Close();
      delete pipe;
      return false;
     }
   ArrayFree(iBuffer);
   uint res=0;
   do
     {
      res=pipe.ReadArray(iBuffer);
     }
   while(res==0 && !IsStopped());
   
   if(res>0)
     {
      string result=CharArrayToString(iBuffer,0,WHOLE_ARRAY,CP_UTF8);
      if(result!="Ok")
        {
         Comment(result);
         pipe.WriteArray(Close);
         pipe.Close();
         delete pipe;
         return false;
        }
     }
   PrevMessage=message;
   pipe.WriteArray(Close);
   pipe.Close();
   delete pipe;
   Comment("");
   return true;
  }

4.3. ユーザーアプリケーション

結論として、クラウドストレージからデータを受信するユーザーアプリケーションを作成し、チャート上にグラフィカルオブジェクトを作成して変更することもできます。 前のアプリケーションと同様に、必要なライブラリをヘッダーに含め、使用するパイプの名前を指定する必要があります。

#include<CopyObject.mqh>
#include<Files\FilePipe.mqh>

#define                     Connection       "\\\\.\\pipe\\GoogleBridge"

アプリケーションは、3つの外部パラメータを備えています。クラウドストレージデータの更新の周期を示す時間、チャート上のオブジェクト ID と、アプリケーションがあるときに、チャートからすべての作成されたオブジェクトを削除する必要があります.

sinput int        RefreshTime =  10; //データを更新する時間 (秒)
sinput string     ObjectsID   =  "GoogleDriveBridge";
sinput bool       DeleteAtClose = true;   //閉じるプログラムでチャートからオブジェクトを削除する

グローバル変数 (プロバイダーアプリケーションの場合と同様) で、グラフィカルオブジェクトを使用するためのクラスを宣言し、最後に受信したメッセージを保存するための文字列変数、および uchar 配列にクラウドストレージへの接続を閉じるためのコマンドを記述します。 さらに、タイマーと変数の状態に関するロジック変数を追加して、最後の更新の時刻を格納し、チャートに最後のコメントを表示します。

CCopyObject *CopyObjects;
string PrevMessage;
bool timer;
datetime LastRefresh,CommentStart;
uchar Close[];

OnInit 関数で、グローバル変数とタイマを初期化します。

int()
  {
//---
   CopyObjects = new CCopyObject(ObjectsID);
   PrevMessage="Init";
   timer=EventSetTimer(1);
   if(!timer)
     {
      Comment("Error of set timer");
      CommentStart=TimeCurrent();
     }
   LastRefresh=0;
   StringToCharArray(("Close"),Close,0,WHOLE_ARRAY,CP_UTF8);
   
//---
   return(INIT_SUCCEEDED);
  }

OnDeinit初期化関数で、グラフィカルオブジェクトを操作するためのオブジェクトクラスを削除し、タイマをストップし、コメントを消去し、(必要に応じて) アプリケーションによって作成されたオブジェクトをチャートから削除します。

void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(CopyObjects)!=POINTER_INVALID)
      delete CopyObjects;
   EventKillTimer();
   Comment("");
   if(DeleteAtClose)
     {
      for(int i=0;i<ObjectsTotal(0,0);i++)
        {
         string name=ObjectName(0,i,0);
         if(StringFind(name,ObjectsID,0)==0)
           {
            if(ObjectDelete(0,name))
               i--;
           }
        }
     }
  }

OnTick関数で、タイマの状態を確認し、必要に応じて再アクティブ化します。

void OnTick()
  {
//---
   if(!timer)
     {
      timer=EventSetTimer(1);
      if(!timer)
        {
         Comment("Error of set timer");
         CommentStart=TimeCurrent();
        }
      OnTimer();
     }
  }

OnTimer関数で、10秒より長いチャートに存在するコメントをクリアし、クラウドストレージ (ReadMessage) からデータファイルを読み取るための関数を呼び出します。 データが正常に読み込まれた後、最後のデータ更新の時刻が変更されます。

void OnTimer()
  {
//---
   if((TimeCurrent()-CommentStart)>10)
     {
      Comment("");
     }
   if((TimeCurrent()-LastRefresh)>=RefreshTime)
     {
      if(ReadMessage(ChartID()))
        {
         LastRefresh=TimeCurrent();
        }
     }
  }

クラウドストレージからデータを読み込んで、チャート上のオブジェクトをプロットするための基本的なアクションは、 ReadMessage関数で実行されます。 この関数には1つのパラメータのみがあり、関数が動作するチャート ID があります。この関数で実行される操作は、数段階に分けることができます。

bool ReadMessage(long chart)
  {
   string Name=SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_BASE)+SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_PROFIT);
   CFilePipe *pipe=new CFilePipe();
   if(CheckPointer(pipe)==POINTER_INVALID)
      return false;
  
   int handle=pipe.Open(Connection,FILE_WRITE|FILE_READ);
   if(handle<=0)
     {
      Comment("Pipe doesn't found");
      CommentStart=TimeCurrent();
      delete pipe;
      return false;
     }
   Comment("Send request");
   uchar iBuffer[];
   int size=StringToCharArray(("Read;"+Name+";"),iBuffer,0,WHOLE_ARRAY,CP_UTF8);
   if(pipe.WriteArray(iBuffer)<=0)
     {
      pipe.Close();
      delete pipe;
      return false;
     }
   Sleep(10);
   ArrayFree(iBuffer);
   Comment("Read message");
   
   uint res=0;
   do
     {
      res=pipe.ReadArray(iBuffer);
     }
   while(res==0 && !IsStopped());
   
   Sleep(10);
   Comment("Close connection");
   pipe.WriteArray(Close);
   pipe.Close();
   delete pipe;
   Comment("");
      
   string result=NULL;
   if(res>0)
     {
      result=CharArrayToString(iBuffer,0,WHOLE_ARRAY,CP_UTF8);
      if(StringFind(result,"Error",0)>=0)
        {
         Comment(result);
         CommentStart=TimeCurrent();
         return false;
        }
     }
   else
     {
      Comment("Empty message");
      return false;
     }
   
   if(result==PrevMessage)
      return true;
  
   if(CheckPointer(CopyObjects)==POINTER_INVALID)
     {
      CopyObjects = new CCopyObject();
      if(CheckPointer(CopyObjects)==POINTER_INVALID)
         return false;
     }
   if(CopyObjects.DrawObjects(chart,result))
     {
      PrevMessage=result;
     }
   else
     {
      return false;
     }
   return true;
  }

5. アプリケーションの最初の起動

さて、作業を終え、いよいよ結果を見る時間です。 ブリッジアプリケーションを起動します。 クラウドストレージに接続するために、(Google サービスから受信した) データを含むclient-secret.jsonファイルがアプリケーションフォルダにあることを確認します。 次に、MetaTrader アプリケーションのいずれかを実行します。 初めてクラウドにアクセスする場合、ブリッジアプリケーションは、Google アカウントのサインインページを使用してデフォルトのインターネットアプリケーションを起動します。

Google アカウントのログインページ

Google アカウントの登録時に指定したメールアドレスをエントリーし、次のページ ([次へ] ボタン) に移動します。 次のページで、アカウントにアクセスするためのパスワードをエントリーします。

Google アカウントのパスワード

次のページでは、クラウドストレージへのアプリケーションのアクセス権を確認するメッセージが Google に表示されます。 リクエストされたアクセス権を確認します ([ALLOW] ボタン)。

アクセス権の確認

ブリッジアプリケーションディレクトリに、ドライブブリッジの json サブフォルダが作成されます。 クラウドストレージのアクセストークンを含むファイルが格納されます。 今後、他のコンピュータでアプリケーションを複製する場合、このサブディレクトリもブリッジプログラムと一緒にコピーする必要があります。 これより、手順を繰り返し、クラウドストレージへのアクセスデータを第3者に転送する必要がなくなります。


結論

この記事では、実用的な目的でクラウドストレージを使用して検討しました。 ブリッジアプリケーションは、クラウドストレージにデータをアップロードし、アプリケーションにロードバックするための普遍的なツールです。 グラフィカルオブジェクトを送信するための提案されたソリューションは、リアルタイムで技術的な分析結果を共有することができます。 おそらく、誰かがトレードシグナルを提供するか、このメソッドでチャートの技術的な分析にトレーニングコースを手配することになります。

すべてのトレーダーの成功をお祈りします。