Использование облачных хранилищ для обмена данными между терминалами

Dmitriy Gizlyk | 3 июля, 2017

Введение

Все большее распространение в современном мире получают облачные технологии. Мы можем использовать хранилища разного объема, как платные, так и бесплатные. Можем ли мы их использовать в практическом трейдинге? В этой статье предлагается технология для обмена данными между терминалами, через облачные хранилища.

Вы спросите: зачем для этого вообще использовать облачные хранилища?  Ведь уже есть технологии прямого соединения между терминалами. Но я думаю, у этого подхода есть ряд преимуществ. Во-первых, это анонимность провайдера: пользователи информации обращаются не к его компьютеру, а к облачному серверу. Таким образом, компьютер провайдера защищен от вирусных атак, и он не должен постоянно быть подключен к Сети. Достаточно подключаться только для отправки сообщений на сервер. Во-вторых, в "облаке" может быть практически неограниченное количество провайдеров. И, в-третьих, с ростом количества пользователей не потребуется увеличения мощностей провайдера.

Для примера воспользуемся бесплатным облачным хранилищем от компании Google в размере 15 Гб. Этого для наших целей более чем достаточно.

1. Немного теории

Авторизация на Google drive организована по протоколу OAuth 2.0. Это открытый протокол авторизации, который позволяет предоставлять сторонним приложениям и сайтам ограниченный доступ к защищенным ресурсам авторизованных пользователей, без необходимости передавать учетные данные. Базовый сценарий доступа по протоколу OAuth 2.0 состоит из 4 этапов.

  1. Вначале необходимо получить данные для авторизации (идентификатор клиента и секрет клиента). Эти данные генерируются сайтом и, соответственно, известны сайту и приложению.
  2. Прежде чем приложение сможет получить доступ к личным данным, оно должно получить токен доступа. Один такой токен может предоставлять различную степень доступа, определяемую переменной scope. Во время запроса токена доступа приложение может отправить одно или несколько значений в параметре scope. Для создания этого запроса приложение может использовать как системный браузер, так и запросы веб-служб. Некоторые запросы требуют этапа аутентификации, на котором пользователь входит со своей учетной записью. После входа в систему у пользователя спрашивают, готов ли он предоставить разрешения, запрашиваемые приложением. Этот процесс называется согласием пользователя. Если пользователь предоставляет разрешение, сервер авторизации предоставит приложению код авторизации по которому приложение может получить токен доступа. Если пользователь не предоставляет разрешения, сервер возвращает ошибку.
  3. После того, как приложение получит токен доступа, оно отправляет токен в заголовке авторизации HTTP. Точки доступа действительны только для набора операций и ресурсов, описанных в параметре scope запроса. Например, если выпущен токен доступа для Google Drive, он не предоставляет доступ к контактам Google. Однако приложение может отправлять этот токен доступа в Google Drive несколько раз для выполнения разрешенных операций.
  4. Токены имеют ограниченный срок службы. Если приложению требуется доступ после срока службы одного токена доступа, оно может получить токен обновления, который позволяет приложению получать новые токены доступа.

Сценарий доступа

2. Организация доступа к Google Drive

 Для работы с Google drive нам потребуется учетная запись Google — или новая, или уже имеющаяся.

Прежде чем начать разработку кода нашей программы, проведем подготовительную работу на сайте Google. Для этого мы переходим в консоль разработчиков (для доступа к консоли нужно будет заново залогиниться в аккаунте).

Создаем новый проект для нашего приложения. Перейдем в панель проектов (кнопка "Select a project" или горячие клавиши"Ctrl + O"). На панели проектов создадим новый проект (кнопка "+").

Панель проектов

В открывшейся странице зададим имя нашего проекта, согласимся с условиями использования и подтвердим создание.

Новый проект

Затем в панели проектов выберем его и подключим к нему Google Drive API. Для этого в библиотеке API менеджера выберем "Drive API" и затем активируем указанный API на открывшейся странице, нажав "Enable".

Библиотека APIАктивация API

Вновь открывшаяся страница подскажет нам, что для использования API нам нужны учетные данные. Их мы и сгенерируем (кнопка "Create credentials").

Предупреждение

Консоль Google предложит воспользоваться мастером для выбора типа аутентификации, но нам это не нужно. Сразу нажимаем на "client ID". В следующем окне Google опять нам покажет предупреждение о необходимости настроить страницу подтверждения доступа, что мы и сделаем, нажав на кнопку "Configure consent screen".

Предупреждение

В открывшейся странице можно все поля оставить по умолчанию, заполнив лишь поле "Product name shown to users" (Название программы, отображаемое пользователю). После этого укажем тип нашего приложения как "Other", дадим имя клиенту и нажмем кнопку "Create" (Создать). В ответ на наши действия сервис сгенерирует "client ID" и "client secret"  коды. Их можно скопировать, но в этом нет необходимости: сервис дает возможность загрузить их в виде json файла. Нажимаем "Ok" и затем загружаем json файл с данными для доступа на локальный диск.

На этом подготовительная работа на стороне сервиса закончена, и мы можем приступать к написанию наших программ.

3. Создаем мост между локальными программами и Google drive

Для решения этой задачи я создал отдельную программу, своеобразный мост, который будет получать запросы и данные от советника или индикатора MetaTrader, обрабатывать их, взаимодействовать с Google drive и возвращать данные назад в программы MetaTrader. Привлекательность такого подхода, во-первых, в том, что Google предоставляет библиотеки для работы с Google drive на C#. Это значительно облегчает разработку. Во-вторых, использование сторонней программы избавит сам терминал от "затратных" операций обмена с внешним сервисом. И, в-третьих, это отвязывает нашу программу от платформы и делает ее кроссплатформенной, с возможностью работать с приложениями и под MetaTrader 4, и под MetaTrader 5.

Как я уже говорил ранее, программа-мост будет написана на C# с использованием библиотек от Google. Создаем проект Windows Form в VisualStudio, и сразу с помощью NuGet добавляем к нему библиотеку Google.Apis.Drive.v3.

Далее для работы с Google drive создаем класс 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 файл с кодами доступа. В моем случае это "client-secret.json". Если Вы сохранили файл с другим именем, то укажите соответствующее имя файла в коде функции. После загрузки данных для авторизации будет вызвана асинхронная функция авторизации на сервисе. В случае успешной авторизации в объекте credential сохранится token для последующего доступа. Во время работы в C# не забываем про обработку исключений: в случае возникновения исключения объект credential будет обнулен. 

        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 drive наш "мост" должен выполнять две функции: запись данных на диск и считывание с него нужного файла. Рассмотрим их более детально. Чтобы реализовать эти функции, такие простые на первый взгляд, нам нужно будет написать целый ряд процедур. Все дело в том, что файловая система Google drive отличается от той, с которой мы привыкли работать. Здесь имена и расширения файлов существуют отдельными записями только для поддержания привычного пользователю представления. На самом деле, при сохранении каждому файлу присваивается уникальный идентификатор, под которым он будет храниться. Таким образом, пользователь может сохранить неограниченное количество файлов с одними и теми же именем и расширением. Следовательно, прежде чем обратиться к файлу, мы должны узнать его идентификатор в облачном хранилище. Для этого загрузим список всех файлов на диске и поочередно сравним их имена с заданным.

За получение списка файла будет отвечать функция GetFileList, которая  вернет список классов Google.Apis.Drive.v3.Data.File. Для получения списка файлов с Google drive мы воспользуемся классом Google.Apis.Drive.v3.DriveService из ранее загруженных библиотек. При инициализации этого класса мы передадим в него токен, полученный при авторизации, и имя нашего проекта. Полученный список сохраним в возвращаемую переменную 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;
            }
            // Create Drive API service.
            using (Google.Apis.Drive.v3.DriveService service = new Google.Apis.Drive.v3.DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            }))
            {
                try
                {
                    // Define parameters of request.
                    FilesResource.ListRequest listRequest = service.Files.List();
                    listRequest.PageSize = 1000;
                    listRequest.Fields = "nextPageToken, files(id, name, size)";

                    // List files.
                    result = listRequest.Execute().Files;
                }
                catch (Exception)
                {
                    return null;
                }
            }
            return result;
        }


3.1. Запись данных в облачное хранилище

Для записи файла в облачное хранилище создадим функцию FileCreate. Входные параметры этой функции — имя файла и его содержимое. Возвращать она будет логическое значение о результате операции и идентификатор файла на диске, в случае его удачного создания. За создание файла будет отвечать уже известный нам класс Google.Apis.Drive.v3.DriveService, а для отправки запроса воспользуемся классом 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 drive. Мы пишем программу для обмена данными между несколькими терминалами, которые находятся на разных компьютерах. И мы не можем знать, в какой момент и скольким терминалам потребуется наша информация. Но особенности файловой системы облачного хранилища позволяют нам создавать несколько файлов с одинаковыми именами и расширениями. Это дает нам возможность сначала создать новый файл с новыми данными, и только потом удалить устаревшие данные из облачного хранилища. Этим и займется наша новая функция FileUpdate. Ее входными параметрами будут имя файла и его содержимое, а вернет она логическое значение о результате операции.

В начале функции объявим текстовую переменную new_id и вызовем ранее созданную функцию FileCreate, которая создаст новый файл данных в облаке и вернет в нашу переменную идентификатор нового файла.

Затем мы получим список всех файлов в облаке из функции GetFileList и поочередно сравним их с именем и идентификатором вновь созданного файла. Все ненужные дубликаты будут удалены из хранилища. Здесь мы опять воспользуемся уже известным классом 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. Чтение данных из облачного хранилища

Мы уже создали функции для записи данных в облачное хранилище. Теперь настало время считать их обратно. Как мы помним, прежде чем загрузить файл, нужно получить его идентификатор в облаке. Эта задача возложена на функцию GetFileID. Ее входной параметр — имя искомого файла, а возвращаемое значение — его идентификатор. Логическое построение функции несложное: мы получаем список файлов из функции 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;
        }

После получения идентификатора файла мы можем получить из него нужную нам информацию. Для этого напишем функцию 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 Drive, настало время соединить ее с приложениями MetaTrader. Ведь именно в этом ее основная задача. Это соединение я решил построить при помощи именованных каналов. Работа с ними уже описывалась на сайте, и в язык MQL5 разработчики уже добавили класс CFilePipe для работы с таким типом соединений. Это облегчит нашу работу при создании приложений.

В терминале одновременно могут быть запущены несколько приложений. Поэтому наш "мост" должен уметь обрабатывать несколько подключений одновременно. Используем для этого модели асинхронного многопотокового программирования.

Сразу определимся с форматом сообщений, передаваемых между мостом и приложением. Для чтения файла из облака надо передать команду и имя файла. Для записи файла в облако необходимо передать команду, имя файла и его содержимое. Так как в канале мы будем передавать данные одним потоком, то целесообразно передавать всю информацию одной строкой. Для разделения полей в строке я применил разделитель ";".

Вначале объявим глобальные переменные:

        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;
            // Wait for a client to connect
            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)
                {
                    // Read the request from the client. Once the client has
                    // written to the pipe its security token will be available.

                    while (pipeServer.IsConnected)
                    {
                        if (!ReadMessage(pipeServer))
                        {
                            exit = true;
                            break;
                        }
                    }
                    //Wait for a client to connect
                    AsyncCallback asyn_connected = new AsyncCallback(Connected);
                    pipeServer.Disconnect();
                    pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
                    break;
                }
            }
            finally
            {
                exit = true;
            }
        }

Считывать и обрабатывать запрос от приложений будет функция ReadMessage. Параметром в функцию передается ссылка на объект потока. Результат функции — логическое значение о выполнении операции. Вначале функция считывает запрос от приложения из именованного канала и делит его на поля. Затем распознает команду и выполняет необходимые действия.

В функции я предусмотрел 3 команды:

Для закрытия текущего соединения достаточно вернуть функцией значение false, все остальное сделает вызвавшая ее функция Connected.

Для выполнения запроса чтения файла из облака мы определяем идентификатор файла и считываем его содержимое, используя описанные выше функции 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 Form, поэтому запускаю эту функцию из функции Form1.

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

Осталось перекомпилировать проект и скопировать в папку с программой  json файл с данными для доступа к облачному хранилищу. 
  

4. Создаем приложения в MetaTrader

Перейдем к практическому применению нашей программы. Для начала предлагаю написать программу копирования простых графических объектов.

4.1. Класс для работы с графическими объектами

Вначале задумаемся, какую именно информацию об объекте нам надо передать, чтобы воссоздать на другом графике. Наверное, в первую очередь надо передать тип объекта и его наименование для идентификации. Понадобятся также цвет объекта и его координаты. И тут возникает первый вопрос: сколько и каких координат передавать? К примеру, при передаче информации о вертикальной линии достаточно передать только дату. Для горизонтальной линии передавать надо не дату, а цену. Для трендовой линии нужны две пары координат — и дата, и цена, плюс информация о том, влево или вправо продолжится линия. У разных объектов есть как общие, так и уникальные параметры. Но в MQL5 все объекты создаются и изменяются четырьмя функциями: ObjectCreate, ObjectSetInteger, ObjectSetDouble и ObjectSetString. Пойдем тем же путем и будем передавать тип параметра, свойство и значение.

Cоздадим перечисление типов параметра.

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. Ее параметр — идентификатор нужного графика, а возвращать она будет текстовое значение для отправки в облачное хранилище с перечнем параметров объекта и их значениями. Возвращаемая строка должна быть структурирована, чтобы эти данные потом можно было прочесть. Условимся, что данные о каждом объекте будут взяты в фигурные скобки, разделителем между параметрами будет знак "|", а разделителем между параметром и его значением — знак "=".  В начале описания каждого объекта указывается его наименование и тип, а затем вызывается функция описания объекта, соответствующая его типу.

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. Ее параметрами будут идентификатор графика и наименование объекта. Возвращать функция будет структурированную строку с параметром объекта. Например, для горизонтальной линии нужно будет передать цену, цвет, стиль и толщину линии, а также указать, отображать линию перед графиком или на заднем фоне. При этом не забываем перед свойством параметра указать его тип из созданного ранее перечисления.

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. Ее параметры — идентификатор графика, на который надо выводить объекты, и полученное сообщение. Возвращать функция будет логическое значение о выполнении операции.

Алгоритм функции включает в себя несколько этапов:

bool CCopyObject::DrawObjects(long chart,string message)
  {
   //--- Split message to objects
   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;
  
   //--- Split every object message to settings
   for(int i=0;i<total;i++)
     {
      string settings[];
      int total_settings=StringSplit(objects[i],'|',settings);
      //--- Search name and type of object
      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 name or type of object not found go to next object
      if(Objects[i].name==NULL || Objects[i].type==-1)
         continue;
      //--- Search object on chart
      int subwindow=ObjectFind(chart,Objects[i].name);
      //--- if object found on chat but it not in main subwindow or its type is different we delete this oject from chart
      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 object doesn't found create it on chart
      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;
  }

Функция присвоения свойств, полученных в сообщении, объекту графика универсальна и применима к любым типам объектов. Параметрами в нее передаются идентификатор графика, наименование  объекта и строковый массив параметров. Каждый элемент массива делится на тип операции, свойство, модификатор и значение. Полученные значения присваиваются объекту через функцию, соответствующую типу операции. 

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;
  }

После вывода объектов на график нужно сравнить существующие на графике объекты с переданными в сообщении. С графика удаляются "лишние" объекты, содержащие требуемый идентификатор, но отсутствующие в сообщении (это объекты, удаленные провайдером). За это отвечает функция DeleteExtraObjects. Ее параметры — идентификатор графика и массив структур, содержащий наименование и тип объекта.

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. Приложение-провайдер

Мы постепенно приближаемся к финалу. Создадим программу-провайдер, которая будет собирать и отправлять данные об объектах в облачное хранилище. Выберем для этого форму эксперта. Внешний параметр будет только один:  логическая переменная SendAtStart, которая определяет, надо ли отправлять данные сразу после загрузки программы в терминал.

sinput bool       SendAtStart =  true; //Send message at Init

В шапке приложения подключим нужные нам библиотеки — это созданный выше класс для работы с графическими объектами и базовый класс для работы с именованными каналами. Также пропишем наименование канала, к которому будет подключаться приложение.

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

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

В глобальных переменных объявим класс для работы с графическими объектами, строковую переменную для сохранения последнего отправленного сообщения и массив типа uchar, в который запишем команду для закрытия соединения с облачным хранилищем.

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

В функции OnInit инициализируем глобальные переменные и запустим функцию отсылки информации в облачное хранилище (при необходимости). 

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

Основные же операции будут выполняться в функции 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: время в секундах, указывающее периодичность обновления данных с облачного хранилища, идентификатор объектов на графике и логическое значение, указывающее необходимость удалить все созданные объекты с графика при закрытии приложения.

sinput int        RefreshTime =  10; //Time to refresh data, sec
sinput string     ObjectsID   =  "GoogleDriveBridge";
sinput bool       DeleteAtClose = true;   //Delete objects from chart at close program

В глобальных переменных приложения, как и в приложении-провайдере, мы объявим класс для работы с графическими объектами, строковую переменную для сохранения последнего полученного сообщения и массив типа uchar, в который запишем команду для закрытия соединения с облачным хранилищем. Дополнительно добавим логическую переменную о состоянии таймера и переменные для хранения времени последнего обновления и вывода последнего комментария на график.

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

В функции OnInit проведем инициализацию глобальных переменных и таймера.

int OnInit()
  {
//---
   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. У этой функции только один параметр — идентификатор графика, с которым функция будет работать. Выполняемые в функции операции можно разделить на несколько этапов:

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, и перейти к следующей странице (кнопка NEXT). На следующей странице вводим пароль доступа к учетной записи.

Пароль доступа к учетной записи Google

На следующей странице Google попросит подтвердить права доступа приложения к нашему облачному хранилищу. Мы должны ознакомиться с запрашиваемыми правами доступа и подтвердить их (кнопка ALLOW).

Подтверждение прав доступа

В результате в каталоге с нашей программой-мостом будет создан подкаталог drive-bridge.json. В нем хранится файл с токеном доступа к облачному хранилищу. В дальнейшем, при тиражировании приложения на другие компьютеры, вместе с программой-мостом должен быть скопирован и этот подкаталог. Это избавит от необходимости повторять процедуру и передавать данные для доступа к облачному хранилищу третьим лицам.

Файл разрешений в подкаталоге программы

Заключение

В этой статье мы рассмотрели один из примеров использования облачных хранилищ в практических целях. Созданная нами программа-мост — универсальное средство для выгрузки данных в облачное хранилище и обратной их загрузки в наши приложения. Предложенное решение для транслирования графических объектов позволит вам обмениваться с коллегами своими результатами технического анализа в реальном времени. Возможно, кто-то решит подобным образом транслировать сигналы для трейдинга или проводить уроки по техническому анализу графиков.

Желаю всем успешной торговли.