简介
在现今的世界中,云技术正变得越来越流行,我们可以使用付费或者免费的存储服务,还有各种空间大小。但是是否可能在实际交易中使用它们呢?本文提出了一种技术,可以使用云存储服务来进行终端之间的数据交换。
您也许会问,当我们已经有了方案在终端之间直接联系的时候,我们为什么还需要云存储来这样做,但是我想,这种方法有一些优点。首先,提供者可以保持匿名: 用户访问的是云服务器而不是提供者的个人电脑,这样,提供者的计算机可以防止病毒攻击,并且它不需要永久连接到互联网,它只要在向服务器发送数据时做连接就可以了。第二,云可以包含不限数量的虚拟提供者。并且还有第三点,当他们的用户增加时,提供者不需要提高它们的计算性能。
让我们使用由Google提供的免费15GB的云存储作为例子,这对我们的任务来说是足够了。
1. 一点历史
Google Drive(谷歌云盘) 的认证是通过 OAuth 2.0 协议的,这是一项开放的认证协议,第三方应用程序和网站可以对认证用户保护的资源进行有限的访问而不需要传输密码信息。基本的 OAuth 2.0 访问场景包含4步,
- 首先,您需要取得用于认证的数据 (客户的 ID 和密码),这些数据是由网站生成的,并且网站和应用程序都是知道的。
- 在应用程序能够访问个人数据之前,它应当收到访问令牌,一个这样的令牌可以提供由'scope'变量定义的不同级别的访问。当请求访问令牌时,应用程序可以在'scope'参数中发送一个或多个值,为了创建这个请求,应用程序可以使用系统浏览器和 web service 请求。有些请求需要在用户登录他们账户的时候有一个认证步骤,在登录之后,用户被询问,他们是否准备给应用程序请求以许可,这个过程被称为用户同意(user consent)。如果用户提出同意,认证服务器就给应用程序提供认证编码,使得应用程序可以获得访问令牌,如果用户没有同意,服务器就返回错误。
- 在应用程序收到访问令牌之后,它会以HTTP认证头的形式发送它,访问点只有在请求的'scope'参数中有描述的操作和资源时才是有效的,例如,如果许可了 Google Drive的访问令牌,它不会提供对 Google contacts(谷歌联系人)的访问,但是,应用程序可以对Google Drive多次发送访问令牌以进行允许的操作。
- 令牌的生命周期是有限的,如果应用程序在访问令牌过期后还需要访问,它可以取得一个更新令牌来允许应用程序取得新的访问令牌。
2. 准备访问 Google Drive
为了操作 Google Drive, 我们需要一个 Google 账户。
在开发应用程序代码之前,让我们在 Google 网站上进行一些准备工作。为此,要访问developers console(开发人员控制台) (再次登录到您的账户才能访问),
为应用程序创建一个新的项目。转到项目面板 ("选择一个项目"按钮或者 Ctrl + O). 创建一个新的项目 (+).
在新打开的页面,设置项目名称,同意使用声明并确认创建。
从面板中选择新项目并把它与Google Drive API关联,为此, 在管理器 API 开发库中选择 Drive API,并且在新的页面点击Enable(启用)来激活它。
新的页面提示我们创建信任以使用 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 EA 或者指标中接收请求和数据,处理它们,再与 Google Drive 交互并把数据返回给 MetaTrader 应用程序。使用这种方法的优点,首先是 Google 提供了开发库用于使用 C# 操作 Google Drive,这使开发更加方便。其次,使用第三方应用程序来防止终端进行因外部服务带来的“资源消耗严重”的交易操作。第三,这可以使我们的应用程序不必与平台绑定,并且可以使它具有跨平台的能力,可以同时用于 MetaTrader 4 和 MetaTrader 5 应用程序。
就像我之前说过的,桥梁应用程序将会使用 Google 开发库,以 C# 语言开发。让我们在 VisualStudio 中创建 Windows Form 项目,并且使用 NuGet 加上 Google.Apis.Drive.v3 开发库。
下一步,让我们创建 GoogleDriveClass 类用于操作 Google Drive:
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 的文件系统和我们熟悉的不同,在这里,文件的名称和扩展名是各自独立的记录,只是用于保持显示功能,实际上,当保存的时候,在存储中每个文件都被赋予唯一的ID,这样,用户可以使用相同的名称和扩展名保存不限数量的文件,所以,在访问文件之前,我们需要知道它在云存储中的ID。为此,要载入盘中所有文件的列表并挨个把它们的名称与指定名称作比较。
GetFileList 函数就是负责取得文件列表的,它会返回 Google.Apis.Drive.v3.Data.File 类的列表。让我们使用之前下载的开发库中的 Google.Apis.Drive.v3.DriveService 类来从 Google Drive 中取得文件列表,当初始化类的时候,我们向它传入登录时取得的令牌以及我们项目的名称,结果的列表保存在返回的 result 变量中。如果有异常发生,该变量会重置为0。如有必要,我们应用程序中的其它函数会请求和处理该文件列表。
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; } // 创建 Drive 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.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 函数,它会在云中创建一个新的数据文件并把新文件的ID返回给我们的变量,
然后我们使用 GetFileList 函数取得云存储中所有文件的列表,把它们挨个与新创建文件的名称和ID相比较,所有不需要的重复内容会从存储中删除。这里我们再次使用已经了解的 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之后,我们就能从中取得我们所需的数据了。为此,我们需要 FileRead 函数, 传入所需的文件ID,而函数会返回它的内容。如果没有成功,函数会返回一个空的字符串。和以前一样,我们需要 Google.Apis.Drive.v3.DriveService 类来创建一个连接,然后使用 Google.Apis.Drive.v3.FilesResource.GetRequest 类来创建请求。
public string FileRead(string id) { if (String.IsNullOrEmpty(id)) { return ("错误. 没有找到文件"); } 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 类用于操作这个连接类型,这将使我们在创建应用程序时工作更加简单。
终端允许运行多个应用程序,所以,我们的“桥梁”应该能够同时处理多个连接。让我们为此使用异步多线程编程模式。
我们应该定义在桥梁和应用程序之间传输消息的格式,为了从云中读取文件,我们应该传入命令和文件名称。为了把文件写到云中,我们应该发送命令,文件名和它的内容。因为在管道中数据的传输是单线程的,所以可以在一个字符串中传入整个信息,我使用了 ";" 作为字符串中的栏位分隔符。
首先,让我们声明全局变量:
- Drive — 之前创建的用于操作云存储的类;
- numThreads — 设置同时运行的线程数量;
- pipeName — 用于保存我们管道名称的字符串变量;
- servers — 运行线程的数组.
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 delegate(委托). 如果有异常发生,线程会重新启动。
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) { // 从客户端读取请求. 当客户端 // 写入管道时,它的安全令牌就可用了. 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 函数读取和处理来自应用程序的请求。一个线程对象的引用被传入函数作为参数,函数的运行结果是逻辑值。首先,该函数从命名管道读取应用程序的请求并把它分成栏位,然后它会识别命令并进行需要的操作,
函数支持三个命令:
- Close — 关闭当前的连接;
- Read — 从云存储读取文件;
- Write — 把文件写到云存储.
为了关闭当前连接,函数只要简单返回 false,调用它的 Connected 函数会处理剩余的事情。
为了执行文件读取请求,我们应当定义文件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 = "错误 " + 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()) ?"完成" : "错误"); } catch (Exception e) { result = "错误 " + 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。我们将遵照相同的方法,传递参数类型,属性和数值。
让我们创建参数类型的枚举。
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 和接收的消息。该函数返回操作执行结果的逻辑值。
函数的算法包含几个步骤:
-
把字符串消息根据对象分成字符串数组;
-
把每个对象数组元素分成参数的数组;
-
在参数数组中寻找名称和对象类型. 把我们的ID加到名称中;
-
根据获得的名称在图表上寻找对象;如果对象不在主窗口或者它的类型与消息中指定的不同,就删除它;
-
如果还没有对象或者在前一步中被删除,就在图表上创建一个新的对象;
-
把消息中接收到的对象属性传输到我们图表上的对象中(使用另外的 CopySettingsToObject 函数);
-
从图表上删除不需要的对象 (由另外的 DeleteExtraObjects 函数进行)。
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); //--- 如果在图表上找到了对象,但是它不在主窗口或者它的类型不同,我们就从图表上删除这个对象 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; }
用于把在消息中接收到的属性赋予到图表对象的函数是通用的,可以用于任何对象类型。图表 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交易的形式执行它。只有一个外部参数: the SendAtStart 逻辑值,它定义了是否应该在应用程序下载到图表的时候就立即发送数据。
sinput bool SendAtStart = true; //在 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函数中进行的,它会用图表 ID 作为输入参数,它的算法可以被分为几个步骤:
-
检查用于操作图形对象类的状态,如有必要重新初始化它;
-
使用之前创建的CreatMessage函数来生成要发送到云存储的消息,如果消息为空或者等于上一次发送的,就退出函数;
-
根据图表交易品种生成要发送到云端的文件名称;
-
与我们的桥梁应用程序使用命名管道建立连接;
-
传递命令通过打开的连接把消息以指定文件名发送到云存储;
-
在接收到执行命令的回应之后,发送命令关闭到云端的连接并中断与桥梁应用程序的命名管道;
-
在退出应用程序之前删除用于操作命名管道的对象。
bool SendMessage(long chart) { Comment("发送消息"); 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("没有找到管道"); delete pipe; return false; } uchar iBuffer[]; int size=StringToCharArray(("Write;"+Name+";"+message),iBuffer,0,WHOLE_ARRAY,CP_UTF8); if(pipe.WriteArray(iBuffer)<=0) { Comment("发送请求失败"); 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"
该应用程序含有三个外部参数: 从云存储更新数据的时间秒数, 图表上对象的 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 OnInit() { //--- CopyObjects = new CCopyObject(ObjectsID); PrevMessage="Init"; timer=EventSetTimer(1); if(!timer) { Comment("设置计时器错误"); 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("设置计时器错误"); CommentStart=TimeCurrent(); } OnTimer(); } }
在 OnTimer 函数中,在图表上清除显示时间超过10秒的注释,并且调用函数来从云存储中读取数据文件 (ReadMessage)。在成功载入数据之后,最后数据更新的时间要做修改。
void OnTimer() { //--- if((TimeCurrent()-CommentStart)>10) { Comment(""); } if((TimeCurrent()-LastRefresh)>=RefreshTime) { if(ReadMessage(ChartID())) { LastRefresh=TimeCurrent(); } } }
从云存储载入数据并且在图表上绘制对象的基本操作是在 ReadMessage 函数中进行的。该函数只有一个参数 — 函数工作的图表 ID。 函数中进行的操作可以分为几个步骤:
-
根据图表交易品种生成用于从云存储中读取的文件名称;
-
打开命名管道用于连接到桥梁应用程序;
-
发送从云存储读取数据请求,指定所需的文件;
-
读取请求处理的结果;
-
发送命令关闭到云端的连接并打断与桥梁应用程序的命名管道连接;
-
把取得的结果与之前的消息作比较,如果数据相同,就退出函数;
-
把取得的消息传递给图形元件处理类对象的 DrawObjects 函数;
-
在 PrevMessage 变量中保存成功处理的消息,用于以后和获得的数据作比较。
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("没有找到管道"); CommentStart=TimeCurrent(); delete pipe; return false; } Comment("发送请求"); 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("读取消息"); uint res=0; do { res=pipe.ReadArray(iBuffer); } while(res==0 && !IsStopped()); Sleep(10); Comment("关闭连接"); 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("消息为空"); 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 账户时提供的电子邮件地址,然后转到下一个页面 (NEXT 按钮). 在下一个页面中,输入用于访问账户的密码。
在下一个页面中,Google 会请你确认应用程序对云存储的访问权限。回顾所需的访问权限并确认它们 (ALLOW 按钮).
在桥梁应用程序目录中会创建 drive-bridge.json 子目录,它保存着用于访问云存储的访问令牌文件。在未来,当把应用程序复制到其他电脑上的好似后,这个子目录也要和桥梁程序一起复制。这使我们不用重复传输第三方的云存储访问数据的过程。
结论
在本文中,我们检验了使用云存储作为实际应用的目标。桥梁应用程序是一个通用工具,可以用于向云存储中上传数据以及把它们读回我们的应用程序。提出的用于传输对象的方案使您可以实时与您的同事共享您的技术分析结果,也许,有人会决定使用这种方法提供交易信号或者安排图表技术分析的训练过程。
祝你们都能成功交易。