HTML 中的图表

Victor | 8 一月, 2014

简介

MetaTrader 5 很有可能是一个完全自给自足的产品,并不需要额外的扩展。MetaTrader 5 提供与经纪人的连接,显示报价,允许我们使用各种各样的指标来进行市场分析,当然,还使交易者有机会进行交易操作。显然,因为 MetaTrader 5 主要专注于轻松交易,它不能——在技术上也不应该是一个专为数学方法的研究、分析以及多媒体内容的创建等而设计的绝对通用的工具。

此外,软件产品的过度通用性也最终会导致其效率、可靠性和安全性的降低。另一方面,在某些情形下,用户可能需要某些额外的功能,尤其是交易者为具有各个领域的专业知识和教育背景的人。因此,任何额外的功能可能提高交易平台的吸引力,当然,如果它们是以非常简单的方式实现的,并且没有以其可靠性和安全性为代价的话。

在本文中,我们将考虑此类补充中的一种,这种补充提供依据从客户端获得的数据创建和显示图表的机会。

每个程序都必须做最其擅长的事情。如果我们遵守此原则,则让我们使 MetaTrader 5 负责与经纪人进行交易,收集和处理收到的信息,并使用另一个针对这些用途的程序来负责信息的图形显示。

WEB 浏览器

现今很难找到一台没有安装 Web 浏览器的计算机。长久一来,浏览器一直在进化和改进。现代浏览器的运行相当可靠、稳定,并且最重要的是——免费。考虑到 WEB 浏览器实际上是访问互联网的基本工具,大多数用户都熟悉它,在使用它时几乎不会遇到什么困难。

现代浏览器的用途是如此之广,以至于我们已经习惯通过 Web 浏览器来观看视频、聆听音乐、玩游戏以及进行很多其他活动。因此,现今 WEB 浏览器是一个完善的工具,用于显示能够以各种格式表示的不同类型的信息。

必须指出,目前有几个非常流行的 WEB 浏览器:InternetExplorerMozilla FirefoxGoogle ChromeOpera。这些浏览器相互之间在软件实施和用户界面方面可能有显著不同。然而在理论上,它们应完全支持在网络中采用的针对信息交换的基本标准,主要涉及 HTML 语言标准。

在实践中,尽管开发人员都很努力,浏览器在实施某些协议或技术方面仍然有某些个性。如果我们确定某个浏览器因为其特性而不适合我们,则可以通过向我们的计算机安装一个或几个其他 WEB 浏览器来轻松地解决此问题。即使对 Firefox 等浏览器狂热支持的人在他们的系统中同时也至少安装有 Internet Explorer。

尽管 WEB 浏览器是作为客户端开发的,提供与远程服务器的互动,但是它们也可用于显示存储在您的计算机中的本地信息。查看以前保存在您的计算机中的 WEB 页面即是这样的例子。浏览器不需要接入互联网就可显示本地页面。

因此,运行在离线模式下的 WEB 浏览器,对于用于扩展 MetaTrader 5 客户端的图形能力的程序而言,是一个非常有吸引力的候选角色。要使用它,您既不需要购买昂贵的浏览器、进行麻烦和冗长的安装,也不需要学习如何使用新的软件产品。

因此,在本文的后面,我们将讨论使用 WEB 浏览器,依据在 MetaTrader 5 中获得的数据构建图表。

HTML 和 JavaScript

通过选择 WEB 浏览器作为我们的扩展,让我们为自己制定一个我们将严格遵守的基本原则 - 创建的 HTML 页面的显示必须在没有本地或远程 WEB 服务器的情况下进行。即我们不会在我们的计算机上安装任何服务器软件,并且显示我们的页面不需要接入网络。我们创建的 HTML 页面应只能通过 WEB 浏览器显示,并且应位于我们的计算机上。此原则将最大程度地减少与因接入外部网络而可能导致的安全性能下降有关的风险。

仅使用 HTML 4 的功能来进行信息显示,我们可以创建具有表格、格式化文本和图像的 WEB 页面,但是这些机会并不能让我们完全满意,因为我们的目标是依据从 MetaTrader 5 收到的数据创建完善的图表。

在大多数情形下,当前往不同的网站时我们在浏览器中看到的内容是使用 HTML 扩展创建的。一般而言,这些扩展在服务器端执行,因此不符合我们的目的。能够在浏览器端工作并且不需要服务器软件的技术,例如 Macromedia Flash、JavaScript 和 Java,可能会让我们产生兴趣。

如果要在浏览器端执行 Macromedia Flash 和 Java 应用,我们至少需要安装额外的插件,而用 JavaScript 编写的用户程序由浏览器直接执行的。所有常见的 WEB 浏览器都有它们自己的内置 JavaScript 解释程序。为了避免安装任何额外的软件或插件,让我们选择 JavaScript

因此,在后文中,我们将仅使用支持 MQL5 的 MetaTrader 5 以及支持 HTMLJavaScript 的 WEB 浏览器。不需要其他软件。应指出,HTML 页面仅仅是文本文件。因此,要创建 HTML 文档,我们可以使用任何文本编辑器。例如,我们可以在 MetaEditor 5 中创建和编辑 HTML 代码。在撰写本文时,HTML 代码的编辑是在 Opera @ USB v10.63 浏览器中进行的,该浏览器允许您编辑页面内容,保存修改后的页面,以及预览显示结果。

不熟悉 HTML 和 JavaScript 语言的人可能担心会在掌握它们时遇到某些困难。为了有利于我们的任务,并且避免深入学习 HTML 和 JavaScript,我们将尝试使用以此技术为基础的现成解决方案。因为在本文的范围内,我们的目标仅限于构建图表,因此我们将使用专为此目的而编写好的现成 JavaScript 库。

Emprise JavaScript Charts 是一个相当先进的图形库。或许读者有兴趣通过提供的链接更好地掌握它,但是这个图形库并不是完全免费的。因此,让我们转向免费的库,例如 Dygraphs JavaScript Visualization LibraryHighcharts charting library。Dygraphs 因其简洁而极具吸引力,而 Highcharts 库包含大量的功能,看起来更加通用。尽管 Highcharts 库大约有 75 KB,并且需要额外的 jQuery 库,该库大约有 70 KB,我们仍然选其作为我们的库。

您可以在我们的网站 http://www.highcharts.com/ 上的 "Demo Gallery" 部分熟悉 Highcharts 库。对于每个示例,您可以通过单击 "View options"(查看选项)来查看其 JavaScript 源代码。有关库的详细说明位于 "Documentation/Options Reference"(说明文档/选项参考)部分,在这个部分中,您还可以找到使用不同选项的很多例子。因为 JavaScript 代码的丰富性以及与 MQL 编程器语法的不同,此库的使用似乎相当复杂。但事实并非如此。考虑简单 HTML 文件的第一个例子,该文件采用库来显示图表。

作为一个例子,让我们在 Notepad 编辑器中创建一个名为 Test_01.htm 的文本文件,并复制以下使用库的简单例子。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Example</title>
<!-- - -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"
             type="text/javascript"></script>
<script src="/js/highcharts.js" type="text/javascript"></script>
<!-- - -->
<script type="text/javascript">
var chart1;
$(document).ready(function(){
  chart1 = new Highcharts.Chart({
    chart: {renderTo: 'container1'},
    series: [{data: [29.9, 71.5, 106.4, 129.2, 144.0, 176.0, 135.6, 148.5, 216.4, 194.1, 95.6, 54.4]}]
  });
});
</script>
<!-- - -->
</head>
<body>
<div id="container1" style="width: 700px; height: 400px "></div>
</body>
</html>

示例代码被注释分为四个部分。

第一部分,代码的顶部包含普通的 HTML 页面标记。目前,我们对此部分的代码没有特别兴趣。

接着是另一部分,这部分包含两个 <script> 标记。在第一个标记中,我们向浏览器发出一个指令,要求从网站 ajax.googleapis.com 下载库代码 jquery.min.js。第二个标记假定在服务器端,目录 /js/ 包含浏览器必须下载的库 highcharts.js。以前已经决定,在显示我们的页面时不应连接到任何外部资源,因此必须更改这部分的代码。

在更改之后,这部分的代码如下所示

<script src="jquery.min.js" type="text/javascript"></script>
<script src="highcharts.js" type="text/javascript"></script>

在这个例子中,我们发出指令,要求从我们的 HTML 文件所在的目录下载这两个库,即从当前目录下载。为了让浏览器下载库,必须首先分别从 ajax.googleapis.com 和 http://www.highcharts.com 下载它们,然后将它们复制到我们的 HTML 文件所在的相同目录。也可以在文末的附件中找到这两个库。

在代码的下一部分创建了 Highcharts.Chart 类的一个对象。参数 "renderTo:'container1'" 指出图表将显示在名为 "container1" 的 HTML 元素中,参数 "data" 定义要显示在图表中的数据。如我们在本例中所见,数据的定义方式与参数相同 - 在创建 Highcharts.Chart 类的对象期间。通过简单的更改,我们将显示数据的定义放到一个单独的代码部分,这样允许我们在需要显示多个图表时能够对它们的数据分组。

在我们的例子的最后一部分,标记 <div> 声明一个名为 "container1" 的 HTML 元素,并且指出此元素的尺寸。如前文所述,这是将用于构建图表的 HTML 元素,其大小由在标记 <div> 中指定的元素 "container1" 的尺寸确定。

考虑到所做的更改之后,我们的示例代码如下所示:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Example</title>
<!-- - -->
<script src="jquery.min.js" type="text/javascript"></script>
<script src="highcharts.js" type="text/javascript"></script>
<!-- - -->
<script type="text/javascript">
var dat1 = [29.9, 71.5, 106.4, 129.2, 144.0, 176.0, 135.6, 148.5, 216.4, 194.1, 95.6, 54.4];
</script>
<!-- - -->
<script type="text/javascript">
var chart1;
$(document).ready(function(){
  chart1 = new Highcharts.Chart({
    chart: {renderTo: 'container1'},
    series: [{data: dat1}]
  });
});
</script>
<!-- - -->
</head>
<body>
<div id="container1" style="width: 700px; height: 400px "></div>
</body>
</html>

可以从文末的附件复制此测试案例和所有库。Test_01.htm 示例文件以及库文件位于相同的 \Test 文件夹中,因此,我们只需要双击 HTML 文件 Test_01.htm 即可查看我们的工作成果。

必须记住,要正常显示此测试页面,应在 WEB 浏览器中允许 JavaScript 的执行。因为出于安全目的,浏览器允许您禁用此选项,它有可能被关闭。如此一来,我们应能看到下图:

Test_01.htm

图 1. Test_01.htm 

这是我们的第一个测试图表,尽管此技术表面上很复杂,但是其创建并不需要太长的时间。

我们应指出以此方式创建的显示图表的某些特点。在复制的目录中,打开文件 Test_01.htm,如果 WEB 浏览器允许您放大查看的页面,您将发现,即使放大很多倍,图表的质量也不会变差

这是因为此图表不是诸如 PNG 或 JPEG 文件等静态图像,在放大或缩小为其绘图分配的区域后会重绘图像。因此,此类图像不能用我们通常喜欢的图片保存方式保存到磁盘。因为图表是采用 JavaScript 创建的,我们必须指出,不同的浏览器具有它们自己的对该语言的内置解释程序,可能并不始终都是按相同的方式执行程序。

使用 JavaScript 创建的图表在使用不同的浏览器查看时可能会有所不同。大多数情况下,与其他浏览器相比,这些差异最常出现在 Internet Explorer 中。

但是我们希望 JavaScript 库的创建者能够考虑让他们的代码与最流行的 WEB 浏览器具有可能的最大兼容性。

MetaTrader 5 和 MQL5

在以上例子中,要在图表中显示的数据是在创建 HTML 页面期间手动设定的。为了安排将数据从 MetaTrader 5 传输到创建的图表,我们使用最简单的方法。让 MetaTrader 5 将数据记录到一个单独的文件,然后在显示图表时从该文件将数据载入浏览器。让我们编写一个包含 HTML 页面的例子,该例子通过从一个文件和 MQL5 中创建此文件的脚本下载数据,从而显示图表。

对于 HTML 文件,我们将使用以前创建的文件 Test_01.htm(先对其进行一些小的更改)。我们将修改后的文件称为 example1.htm。做出的更改将会减少,因为以下代码行:

<script type="text/javascript">
var dat1 = [29.9, 71.5, 106.4, 129.2, 144.0, 176.0, 135.6, 148.5, 216.4, 194.1, 95.6, 54.4];
</script>
将被替换为
<script type="text/javascript">
var dat1=[0];
</script>
<script src="exdat.txt" type="text/javascript"></script>

现在,浏览器在下载 HTML 页面时也需要下载 exdat.txt 文本文件,在该文件中,要显示在图表中的值将被赋予 dat1 数组。此文件应包含一段 JavaScript 代码。可以使用对应的脚本在 MetaTrader 5 中轻松创建此文件。

以下提供了此类脚本的一个例子。

//+------------------------------------------------------------------+
//|                                                     Example1.mq5 |
//|                        Copyright 2010, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
  int i,n,fhandle;
  double gr[25];
  string str;
  
  n=ArraySize(gr);
  for(i=0;i<n;i++)
    {
    gr[i]=NormalizeDouble(MathSin(i*3*2*M_PI/n),4);
    }

  str=DoubleToString(gr[0],4);
  for(i=1;i<n;i++)
    {
    str+=","+DoubleToString(gr[i],4);
    }
  
  ResetLastError();
  fhandle=FileOpen("exdat.txt",FILE_WRITE|FILE_TXT|FILE_ANSI);
  if(fhandle<0){Print("File open failed, error ",GetLastError());return;}
  
  FileWriteString(fhandle,"dat1=["+str+"];\n");
  
  FileClose(fhandle);
  }
//+------------------------------------------------------------------+

为了存储显示的数据,此脚本使用容纳 25 个元素的 gr[] 数组。作为一个例子,此数组被填以正弦函数的值,并且四舍五入到四位小数。当然,此数组也可以被填以任何其他更加有用的数据。

此外,此数据被格式化并组合为一个文本字符串。为了减小生成的文本文件的大小,仅将 gr[] 数组元素精确到四位小数的值放置到字符串。为此,我们使用 DoubleToString() 函数。

组合文本字符串 str 之后,将其存储到 exdat.txt 文件中。如果成功执行此脚本,将在客户端的 \MQL5\Files 子文件夹中创建 texdat.txt 文本文件;如果该文件已经存在,则会被覆盖。

本文末尾的附件部分提供了 jquery.min.js、highcharts.js、Example1.mq5、Example1.htm 和 exdat.txt 文件。这五个文件位于目录 \Example1 中。要简单地查看结果,只需要复制此示例,并在目录 \Example1 中打开文件 Example1.htm。将依据来自 exdat.txt 文件的数据建立图表。

Example1.htm 

图 2. Example1.htm

当然,要运行 Example1.mq5 脚本,该脚本应位于客户端的 \MQL5\Scripts 文件夹中并经过编译。

如前文所述,在启动脚本之后,将在 \MQL5\Files 文件夹中创建 exdat.txt 文件,但是在我们的例子中,HTML 文件、来自库的文件以及数据文件都必须位于同一文件夹中。因此,我们必须将文件 jquery.min.js、highcharts.js 和 Example1.htm 复制到 \MQL5\Files 文件夹,或将 exdat.txt 文件复制到这些文件所在的文件夹。

在本例中,HTML 页面和库存储在不同的文件中。在设计阶段,项目的不同部分位于单独的文件中可能会有用处。例如,这样有助于避免在编辑 HTML 文件时,库的代码出现随机变化。但是在编辑完毕 HTML 页面并且不再进行进一步更改之后,库将直接整合到 HTML 代码文件。

这是有可能的,因为 JavaScript 库只是简单的文本文件。如果我们用一个文本编辑器打开 jquery.min.js 或 highcharts.js,我们会看不到任何能理解的东西,因为库的源代码被最大可能地压缩。

压缩是通过删除服务符号,例如换行符或一系列的空格进行的。经过此类压缩之后,任何格式都已丢失,但是文本仍然保留为文本,因为文件的类型没有改变。因此,无论浏览器是从带有 extension .js 的外部文件连接库代码,还是从当前 HTML 文件读取以文本格式运行的库代码,两者之间没有区别。

为了组合文件,将 Example1.htm 中的代码行

<script src="jquery.min.js" type="text/javascript"></script>
<script src="highcharts.js" type="text/javascript"></script>

替换为

<script type="text/javascript">

</script>

接着,使用 Notepad 等文本编辑器,打开库 jquery.min.js 的文件,然后通过选择命令 "Select all"(全选)来复制文件的内容。再接着,打开文件 Example1.htm,在标记 <script type=\"text/javascript\"> 和 </script> 之间粘贴复制的库文本。将获得的文件保存为 Example2.htm。以同样的方式,将库 highcharts.js 的内容复制到此文件,将其放在以前复制的库文本和标记 </script> 之间。

作为复制的结果,HTML 文件的大小增加,然而,现在我们不需要为了其正确显示而分开库文件了。在 Folder\Example2 中包含 exdat.txt 数据文件就已经足够,该文件夹包含文件 Example2.htm,并且在本文末尾的附件部分提供了 exdat.txt。

以图形格式表示的交易历史报告

为了更加完整的说明所提议的显示图形信息的方法,我们将创建一个报告,该报告以指定时间间隔显示交易帐户的历史记录。在您于 MetaTrader 5 中选择 "History"(历史记录)选项卡的上下文菜单中的 "Report"(报告)命令时创建的基于 HTML 的报告将用作原型。此报告包含大量不同的特征,以一张表格汇总。假定这些特征以图表格式表示会更加直观,让我们使用 highcharts.js 图形库来显示它们。

在上面的例子中,为了构建图表,我们使用了在此版本的 highcharts.js 库中设置的默认显示参数。

出于实践目的,此选项不会成功,因为在每种情况下我们都必须调整图表的显示以满足个人的具体要求。出于此目的,highcharts.js 库提供各种各样的机会,具有大量可应用于图表的选项。如前文所述,可以在 http://www.highcharts.com 中找到选项列表以及它们的详细描述和示例。

我们不会在图形库选项的描述和其运用细节上踌躇不前,因为本文的主旨仅是建议和演示使用 WEB 浏览器显示从 MetaTrader 5 收到的信息的能力。特别是因为取决于对 WEB 页面的创建的具体要求,也可以使用其他的 JavaScript 库。读者可以独立地选择最适合的库,并且依据实际运用的需要尽可能深入地研究它。

为了显示交易帐户的历史记录,我们创建了 ProfitReport.htm 文件。可以在附件中找到该文件。\Report 文件夹包含 data.txt 文件,该文件含有要显示的数据。文件 data.txt 作为一个例子放置在该文件夹中。

当我们复制 \Report 文件夹并打开 ProfitReport.htm 时,我们将看到以图形格式出现、为本例创建的测试帐户的交易特征。

ProfitReport.htm

图 3. ProfitReport.htm

在创建 ProfitReport.htm 时,我们首先进行了大概的页面布局,并且确定了哪些类型的信息大概在什么位置。

然后我们将使用默认选项的图表放在页面中。

创建此模板后,我们为每个图表单独选择最适合的选项。在完成编辑之后,我们简单地将库的文本复制到页面中。如前文所述,为了正确显示页面,它应位于包含要显示的数据的文件 data.txt 所在的同一目录中。

文件 data.txt 是在 MetaTrader 5 中用 ProfitReport.mq5 脚本创建的。如果成功执行此脚本,则将在 \MQL5\Files 文件夹中创建包含当前活动帐户的交易特征的文件 data.txt。

必须记住,脚本应放在 \MQL5\Scripts 文件夹中并且经过编译。

//-----------------------------------------------------------------------------------
//                                                                   ProfitReport.mq5
//                                          Copyright 2011, MetaQuotes Software Corp.
//                                                                https://www.mql5.com
//-----------------------------------------------------------------------------------
#property copyright   "Copyright 2011, MetaQuotes Software Corp."
#property link        "https://www.mql5.com"
#property version     "1.00"
#property script_show_inputs

#include <Arrays\ArrayLong.mqh>
#include <Arrays\ArrayDouble.mqh>
#include <Arrays\ArrayString.mqh>
#include <Arrays\ArrayInt.mqh>

//--- input parameters
input int nD=30;               // Number of days
//--- global
double   balabce_cur=0;        // balance
double   initbalance_cur=0;    // Initial balance (not including deposits to the account)
int      days_num;             // number of days in the report (including the current day)
datetime tfrom_tim;            // Date from
datetime tend_tim;             // Date to
double   netprofit_cur=0;      // Total Net Profit
double   grossprofit_cur=0;    // Gross Profit
double   grossloss_cur=0;      // Gross Loss
int      totaltrades_num=0;    // Total Trades
int      longtrades_num=0;     // Number of Long Trades
double   longtrades_perc=0;    // % of Long Trades
int      shorttrades_num=0;    // Number of Short Trades
double   shorttrades_perc=0;   // % of Short Trades
int      proftrad_num=0;       // Number of All Profit Trades
double   proftrad_perc=0;      // % of All Profit Trades
int      losstrad_num=0;       // Number of All Loss Trades
double   losstrad_perc=0;      // % of All Loss Trades
int      shortprof_num=0;      // Number of Short Profit Trades
double   shortprof_perc=0;     // % of Short Profit Trades
double   shortloss_perc=0;     // % of Short Loss Trades
int      longprof_num=0;       // Number of Long Profit Trades
double   longprof_perc=0;      // % of Long Profit Trades
double   longloss_perc=0;      // % of Long Loss Trades
int      maxconswins_num=0;    // Number of Maximum consecutive wins
double   maxconswins_cur=0;    // Maximum consecutive wins ($)
int      maxconsloss_num=0;    // Number of Maximum consecutive losses
double   maxconsloss_cur=0;    // Maximum consecutive losses ($)
int      aveconswins_num=0;    // Number of Average consecutive wins
double   aveconswins_cur=0;    // Average consecutive wins ($)
int      aveconsloss_num=0;    // Number of Average consecutive losses
double   aveconsloss_cur=0;    // Average consecutive losses ($)
double   largproftrad_cur=0;   // Largest profit trade
double   averproftrad_cur=0;   // Average profit trade
double   larglosstrad_cur=0;   // Largest loss trade
double   averlosstrad_cur=0;   // Average loss trade
double   profitfactor=0;       // Profit Factor
double   expectpayoff=0;       // Expected Payoff
double   recovfactor=0;        // Recovery Factor
double   sharperatio=0;        // Sharpe Ratio
double   ddownabs_cur=0;       // Balance Drawdown Absolute
double   ddownmax_cur=0;       // Balance Drawdown Maximal
double   ddownmax_perc=0;      // % of Balance Drawdown Maximal
int      symbols_num=0;        // Numbre of Symbols
  
string       Band="";
double       Probab[33],Normal[33];
CArrayLong   TimTrad;
CArrayDouble ValTrad;
CArrayString SymNam;
CArrayInt    nSymb;

//-----------------------------------------------------------------------------------
// Script program start function
//-----------------------------------------------------------------------------------
void OnStart()
  {
  int         i,n,m,k,nwins=0,nloss=0,naverw=0,naverl=0,nw=0,nl=0;
  double      bal,sum,val,p,stdev,vwins=0,vloss=0,averwin=0,averlos=0,pmax=0;
  MqlDateTime dt;
  datetime    ttmp,it;
  string      symb,br;
  ulong       ticket;
  long        dtype,entry;
  
  if(!TerminalInfoInteger(TERMINAL_CONNECTED)){printf("Terminal not connected.");return;}
  days_num=nD;
  if(days_num<1)days_num=1;             // number of days in the report (including the current day)
  tend_tim=TimeCurrent();                                                // date to
  tfrom_tim=tend_tim-(days_num-1)*86400;
  TimeToStruct(tfrom_tim,dt);
  dt.sec=0; dt.min=0; dt.hour=0;
  tfrom_tim=StructToTime(dt);                                            // date from
//---------------------------------------- Bands
  ttmp=tfrom_tim;
  br="";
  if(dt.day_of_week==6||dt.day_of_week==0)
    {
    Band+=(string)(ulong)(ttmp*1000)+",";
    br=",";ttmp+=86400;
    }
  for(it=ttmp;it<tend_tim;it+=86400)
    {
    TimeToStruct(it,dt);
    if(dt.day_of_week==6){Band+=br+(string)(ulong)(it*1000)+","; br=",";}
    if(dt.day_of_week==1&&br==",") Band+=(string)(ulong)(it*1000);
    }
  if(dt.day_of_week==6||dt.day_of_week==0) Band+=(string)(ulong)(tend_tim*1000);

//----------------------------------------
  balabce_cur=AccountInfoDouble(ACCOUNT_BALANCE);                          // Balance

  if(!HistorySelect(tfrom_tim,tend_tim)){Print("HistorySelect failed");return;}
  n=HistoryDealsTotal();                                           // Number of Deals
  for(i=0;i<n;i++)
    {
    ticket=HistoryDealGetTicket(i);
    entry=HistoryDealGetInteger(ticket,DEAL_ENTRY);
    if(ticket>=0&&(entry==DEAL_ENTRY_OUT||entry==DEAL_ENTRY_INOUT))
      {
      dtype=HistoryDealGetInteger(ticket,DEAL_TYPE);
      if(dtype==DEAL_TYPE_BUY||dtype==DEAL_TYPE_SELL)
        {
        totaltrades_num++;                                          // Total Trades
        val=HistoryDealGetDouble(ticket,DEAL_PROFIT);
        val+=HistoryDealGetDouble(ticket,DEAL_COMMISSION);
        val+=HistoryDealGetDouble(ticket,DEAL_SWAP);
        netprofit_cur+=val;                                         // Total Net Profit
        if(-netprofit_cur>ddownabs_cur)ddownabs_cur=-netprofit_cur; // Balance Drawdown Absolute
        if(netprofit_cur>pmax)pmax=netprofit_cur;
        p=pmax-netprofit_cur;
        if(p>ddownmax_cur)
          {
          ddownmax_cur=p;                                 // Balance Drawdown Maximal
          ddownmax_perc=pmax;
          }
        if(val>=0)              //win
          {
          grossprofit_cur+=val;                            // Gross Profit 
          proftrad_num++;                                  // Number of Profit Trades
          if(val>largproftrad_cur)largproftrad_cur=val;    // Largest profit trade
          nwins++;vwins+=val;
          if(nwins>=maxconswins_num)
            {
            maxconswins_num=nwins;
            if(vwins>maxconswins_cur)maxconswins_cur=vwins;
            }
          if(vloss>0){averlos+=vloss; nl+=nloss; naverl++;}
          nloss=0;vloss=0;
          }
        else                    //loss
          {
          grossloss_cur-=val;                                   // Gross Loss
          if(-val>larglosstrad_cur)larglosstrad_cur=-val;       // Largest loss trade
          nloss++;vloss-=val;
          if(nloss>=maxconsloss_num)
            {
            maxconsloss_num=nloss;
            if(vloss>maxconsloss_cur)maxconsloss_cur=vloss;
            }
          if(vwins>0){averwin+=vwins; nw+=nwins; naverw++;}
          nwins=0;vwins=0;
          }
        if(dtype==DEAL_TYPE_SELL)
          {
          longtrades_num++;                          // Number of Long Trades
          if(val>=0)longprof_num++;                  // Number of Long Profit Trades
          }
        else if(val>=0)shortprof_num++;               // Number of Short Profit Trades

        symb=HistoryDealGetString(ticket,DEAL_SYMBOL);   // Symbols
        k=1;
        for(m=0;m<SymNam.Total();m++)
          {
          if(SymNam.At(m)==symb)
            {
            k=0;
            nSymb.Update(m,nSymb.At(m)+1);
            }
          }
        if(k==1)
          {
          SymNam.Add(symb);
          nSymb.Add(1);
          }
        
        ValTrad.Add(val);
        TimTrad.Add(HistoryDealGetInteger(ticket,DEAL_TIME));
        }
      }
    }
  if(vloss>0){averlos+=vloss; nl+=nloss; naverl++;}
  if(vwins>0){averwin+=vwins; nw+=nwins; naverw++;}
  initbalance_cur=balabce_cur-netprofit_cur;
  if(totaltrades_num>0)
    {
    longtrades_perc=NormalizeDouble((double)longtrades_num/totaltrades_num*100,1);     // % of Long Trades
    shorttrades_num=totaltrades_num-longtrades_num;                                 // Number of Short Trades
    shorttrades_perc=100-longtrades_perc;                                           // % of Short Trades
    proftrad_perc=NormalizeDouble((double)proftrad_num/totaltrades_num*100,1);         // % of Profit Trades
    losstrad_num=totaltrades_num-proftrad_num;                                      // Number of Loss Trades
    losstrad_perc=100-proftrad_perc;                                                // % of All Loss Trades
    if(shorttrades_num>0)
      {
      shortprof_perc=NormalizeDouble((double)shortprof_num/shorttrades_num*100,1);     // % of Short Profit Trades
      shortloss_perc=100-shortprof_perc;                                            // % of Short Loss Trades
      }
    if(longtrades_num>0)
      {
      longprof_perc=NormalizeDouble((double)longprof_num/longtrades_num*100,1);        // % of Long Profit Trades
      longloss_perc=100-longprof_perc;                                              // % of Long Loss Trades
      }
    if(grossloss_cur>0)profitfactor=NormalizeDouble(grossprofit_cur/grossloss_cur,2);  // Profit Factor
    if(proftrad_num>0)averproftrad_cur=NormalizeDouble(grossprofit_cur/proftrad_num,2);// Average profit trade
    if(losstrad_num>0)averlosstrad_cur=NormalizeDouble(grossloss_cur/losstrad_num,2);  // Average loss trade
    if(naverw>0)
      {
      aveconswins_num=(int)NormalizeDouble((double)nw/naverw,0);
      aveconswins_cur=NormalizeDouble(averwin/naverw,2);
      }
    if(naverl>0)
      {
      aveconsloss_num=(int)NormalizeDouble((double)nl/naverl,0);
      aveconsloss_cur=NormalizeDouble(averlos/naverl,2);
      }
    p=initbalance_cur+ddownmax_perc;
    if(p!=0)
      {
      ddownmax_perc=NormalizeDouble(ddownmax_cur/p*100,1); // % of Balance Drawdown Maximal
      }
    if(ddownmax_cur>0)recovfactor=NormalizeDouble(netprofit_cur/ddownmax_cur,2); // Recovery Factor

    expectpayoff=netprofit_cur/totaltrades_num;                    // Expected Payoff
    
    sum=0;
    val=balabce_cur;
    for(m=ValTrad.Total()-1;m>=0;m--)
      {
      bal=val-ValTrad.At(m);
      p=val/bal;
      sum+=p;
      val=bal;
      }
    sum=sum/ValTrad.Total();
    stdev=0;
    val=balabce_cur;
    for(m=ValTrad.Total()-1;m>=0;m--)
      {
      bal=val-ValTrad.At(m);
      p=val/bal-sum;
      stdev+=p*p;
      val=bal;
      }
    stdev=MathSqrt(stdev/ValTrad.Total());
    if(stdev>0)sharperatio=NormalizeDouble((sum-1)/stdev,2);    // Sharpe Ratio

    stdev=0;
    for(m=0;m<ValTrad.Total();m++)
      {
      p=ValTrad.At(m)-expectpayoff;
      stdev+=p*p;
      }
    stdev=MathSqrt(stdev/ValTrad.Total());                      // Standard deviation
    if(stdev>0)
      {
      ArrayInitialize(Probab,0.0);
      for(m=0;m<ValTrad.Total();m++)                           // Histogram
        {
        i=16+(int)NormalizeDouble((ValTrad.At(m)-expectpayoff)/stdev,0);
        if(i>=0 && i<ArraySize(Probab))Probab[i]++;
        }
      for(m=0;m<ArraySize(Probab);m++) Probab[m]=NormalizeDouble(Probab[m]/totaltrades_num,5);
      }
    expectpayoff=NormalizeDouble(expectpayoff,2);                  // Expected Payoff  
    k=0;
    symbols_num=SymNam.Total();                                  // Symbols
    for(m=0;m<(6-symbols_num);m++)
      {
      if(k==0)
        {
        k=1;
        SymNam.Insert("",0);
        nSymb.Insert(0,0);
        }
      else
        {
        k=1;
        SymNam.Add("");
        nSymb.Add(0);
        }
      }
    }
  p=1.0/MathSqrt(2*M_PI)/4.0;
  for(m=0;m<ArraySize(Normal);m++)                             // Normal distribution
    {
    val=(double)m/4.0-4;
    Normal[m]=NormalizeDouble(p*MathExp(-val*val/2),5);
    }

  filesave();
  }
//-----------------------------------------------------------------------------------
// Save file
//-----------------------------------------------------------------------------------
void filesave()
  {
  int n,fhandle;
  string loginame,str="",br="";
  double sum;
  
  ResetLastError();
  fhandle=FileOpen("data.txt",FILE_WRITE|FILE_TXT|FILE_ANSI);
  if(fhandle<0){Print("File open failed, error ",GetLastError());return;}
  
  loginame="\""+(string)AccountInfoInteger(ACCOUNT_LOGIN)+", "+
                        TerminalInfoString(TERMINAL_COMPANY)+"\"";
  str+="var PName="+loginame+";\n";
  str+="var Currency=\""+AccountInfoString(ACCOUNT_CURRENCY)+"\";\n";
  str+="var Balance="+(string)balabce_cur+";\n";
  str+="var IniBalance="+(string)initbalance_cur+";\n";
  str+="var nDays="+(string)days_num+";\n";
  str+="var T1="+(string)(ulong)(tfrom_tim*1000)+";\n";
  str+="var T2="+(string)(ulong)(tend_tim*1000)+";\n";
  str+="var NetProf="+DoubleToString(netprofit_cur,2)+";\n";
  str+="var GrossProf="+DoubleToString(grossprofit_cur,2)+";\n";
  str+="var GrossLoss="+DoubleToString(grossloss_cur,2)+";\n";
  str+="var TotalTrad="+(string)totaltrades_num+";\n";
  str+="var NProfTrad="+(string)proftrad_num+";\n";
  str+="var ProfTrad="+DoubleToString(proftrad_perc,1)+";\n";
  str+="var NLossTrad="+(string)losstrad_num+";\n";
  str+="var LossTrad="+DoubleToString(losstrad_perc,1)+";\n";
  str+="var NLongTrad="+(string)longtrades_num+";\n";
  str+="var LongTrad="+DoubleToString(longtrades_perc,1)+";\n";
  str+="var NShortTrad="+(string)shorttrades_num+";\n";
  str+="var ShortTrad="+DoubleToString(shorttrades_perc,1)+";\n";
  str+="var ProfLong ="+DoubleToString(longprof_perc,1)+";\n";
  str+="var LossLong ="+DoubleToString(longloss_perc,1)+";\n";
  FileWriteString(fhandle,str); str="";
  str+="var ProfShort="+DoubleToString(shortprof_perc,1)+";\n";
  str+="var LossShort="+DoubleToString(shortloss_perc,1)+";\n";
  str+="var ProfFact="+DoubleToString(profitfactor,2)+";\n";
  str+="var LargProfTrad="+DoubleToString(largproftrad_cur,2)+";\n";
  str+="var AverProfTrad="+DoubleToString(averproftrad_cur,2)+";\n";
  str+="var LargLosTrad="+DoubleToString(larglosstrad_cur,2)+";\n";
  str+="var AverLosTrad="+DoubleToString(averlosstrad_cur,2)+";\n";
  str+="var NMaxConsWin="+(string)maxconswins_num+";\n";
  str+="var MaxConsWin="+DoubleToString(maxconswins_cur,2)+";\n";
  str+="var NMaxConsLos="+(string)maxconsloss_num+";\n";
  str+="var MaxConsLos="+DoubleToString(maxconsloss_cur,2)+";\n";
  str+="var NAveConsWin="+(string)aveconswins_num+";\n";
  str+="var AveConsWin="+DoubleToString(aveconswins_cur,2)+";\n";
  str+="var NAveConsLos="+(string)aveconsloss_num+";\n";
  str+="var AveConsLos="+DoubleToString(aveconsloss_cur,2)+";\n";
  str+="var ExpPayoff="+DoubleToString(expectpayoff,2)+";\n";
  str+="var AbsDD="+DoubleToString(ddownabs_cur,2)+";\n";
  str+="var MaxDD="+DoubleToString(ddownmax_cur,2)+";\n";
  str+="var RelDD="+DoubleToString(ddownmax_perc,1)+";\n";
  str+="var RecFact="+DoubleToString(recovfactor,2)+";\n";
  str+="var Sharpe="+DoubleToString(sharperatio,2)+";\n";
  str+="var nSymbols="+(string)symbols_num+";\n";
  FileWriteString(fhandle,str);

  str="";br="";
  for(n=0;n<ArraySize(Normal);n++)
    {
    str+=br+"["+DoubleToString(((double)n-16)/4.0,2)+","+DoubleToString(Normal[n],5)+"]";
    br=",";
    }
  FileWriteString(fhandle,"var Normal=["+str+"];\n");

  str="";
  str="[-4.25,0]";
  for(n=0;n<ArraySize(Probab);n++)
    {
    if(Probab[n]>0)
      {
      str+=",["+DoubleToString(((double)n-16)/4.0,2)+","+DoubleToString(Probab[n],5)+"]";
      }
    }
  str+=",[4.25,0]";
  FileWriteString(fhandle,"var Probab=["+str+"];\n");

  str=""; sum=0;
  if(ValTrad.Total()>0)
    {
    sum+=ValTrad.At(0);
    str+="["+(string)(ulong)(TimTrad.At(0)*1000)+","+DoubleToString(sum,2)+"]";
    for(n=1;n<ValTrad.Total();n++)
      {
      sum+=ValTrad.At(n);
      str+=",["+(string)(ulong)(TimTrad.At(n)*1000)+","+DoubleToString(sum,2)+"]";
      }
    }
  FileWriteString(fhandle,"var Prof=["+str+"];\n");
  FileWriteString(fhandle,"var Band=["+Band+"];\n");

  str="";br="";
  for(n=0;n<SymNam.Total();n++)
    {
    str+=br+"{name:\'"+SymNam.At(n)+"\',data:["+(string)nSymb.At(n)+"]}";
    br=",";
    }
  FileWriteString(fhandle,"var Sym=["+str+"];\n");

  FileClose(fhandle);
  }

如我们所见,脚本代码有点麻烦,但这并不是因为任务的复杂性,而是因为大量的交易特征,它们的值需要确定。为了存储这些值,脚本的开头声明全局变量并提供相应的注释。

OnStart() 函数验证客户端是否连接到交易服务器,如果没有,则脚本结束其工作。在没有到服务器的连接时,我们不能定义一个活动帐户并且获取相关信息。

下一步是日期的计算,报告将包含从该日期计起的当前活动帐户的交易数据。对于结束日期,我们使用脚本执行时的当前日期和当前时间的值。包含在报告中的天数,可以在加载脚本时通过改变输入参数 "Number of days"(天数)设置,该参数的默认值为 30 天。一旦我们定义了报告的开始时间和结束时间,便在字符串变量 Band 中定义了一对与周末相对应的时间值。此信息用于在余额表上,对应于星期六和星期日的时间间隔能标记为黄色。

接下来,使用 HistorySelect() 函数,指定间隔的成交和订单历史记录便可获取,并且通过调用 HistoryDealsTotal() 函数,我们确定历史记录中成交的数量。之后,依据成交数量安排一个循环,该循环收集计算交易特征所需的统计数据,并且在循环结束时确定它们的值。

在我们创建脚本时,我们的任务是依据在 MetaTrader 5 中生成的报告保留交易特征的意义。假定脚本计算的特征必须与客户端的帮助文件中给出的描述一致。

可以在以下文章中找到有关访问帐户历史记录和交易特征计算的信息:

大多数的特征计算非常简单,因此在本文中我们不考虑与各个特征的计算有关的操作,而是仅进一步考虑与标准报告其及补充的现有差异。

在客户端生成的报告中,通过顺序显示每次改变时的值来构建余额表,并且 X 刻度反映此类改变的数量。在我们的例子中,为了构建图表,我们使用一个时间刻度。

因此,盈利表与客户端生成的图表有很大不同。为了以实际时间刻度的方式显示平仓的时间,我们选择这种图表构建选项。因此,我们可以在报告期内看到交易活动何时增加或何时减少。

构建图表时,必须记住 MQL5 以自 1970 年 1 月 1 日算起的秒数表示的日期值运行,而图像库要求该值以 1970 年 1 月 1 日后的毫秒数表示。因此,在脚本中收到的日期值必须乘以 1000 才能正确显示。

为了存储平仓时的利润和时间值,脚本使用来自标准库CArrayDoubleCArrayLong 类。每次在循环中检测到产生的成交时,使用 Add() 方法将其信息添加到元素,再将元素添加到数组的末尾。这样允许我们避免提前确定需要的元素数量。数组的大小会随着在成交历史记录中找到的成交的数量而增加。

对于每笔成交,会对所执行的交易品种进行检查,同时保留交易品种的名称和在其上执行的成交的数量。与盈利表一样,此数据通过将其记录到要添加到数组末尾的元素中,在查看历史时累积。为了存储交易品种的名称和成交数量,我们使用标准库中的 CArrayStringCArrayInt 类。

如果成交是在一个交易品种上执行的,则图表中的一列太宽了。为了避免这种情况,数据数组始终包含至少 7 个元素。未使用的元素不会显示在图表中,因为它们的值为零,这样不会让列变得太宽。为了确保在交易品种的数量较少时,列能位于 X 轴的大约中间位置,将数组中无关紧要的元素按顺序插入数组的开头或末尾。

与标准报告的下一个差异是尝试为每笔交易的利润值序列构建概率分布图。

概率密度

图 4. 概率密度

大多数情况下,此类图表以直方图表示。在我们的例子中,通过依据此类直方图的现有列值来构建一条样条曲线,从而创建概率分布图。计算出来的概率密度值在图表外部的左右两侧补充零值。这是必需的,能够让按样条曲线构建图表不会在最后一个已知值时中断,并且持续到图表以外,下降到零。

为了进行比较,在概率密度图上,用灰色突出显示正态分布图,采用将其读取之和等于 1 的方式进行正态化,正如以直方图的值构建的图表一样。在提供的示例报告中,成交的数量不足以提供多少有点可靠的盈利交易值的概率分布估算。我们可以假定在历史记录中有大量成交时,此图表看起来将更加可信。

计算完毕所有交易特征以后,在脚本的末尾调用 filesave() 函数。此函数打开以文本格式将变量的名称和值存储在其中的 data.txt 文件。这些变量的值对应于计算的参数,它们的名称对应于在将参数传输到图形库的函数期间在 HTML 文件中使用的名称。

为了减少在写文件时对磁盘的存取次数,短的代码行被合并为一个长的代码行后才被记录到文件。作为 MetaTrader 5 中的一个惯例,data.txt 文件在目录 MQL5\Files 中创建;如果此文件已经存在,则会被覆盖。为方便起见,您可以将 ProfitReport.htm 文件复制到此目录,并从这个位置运行。

在 MetaTrader 5 客户端中,当以 HTML 格式保存报告时,会在注册为默认的浏览器中自动打开报告。在本文提供的例子中没有实施这种可能性。

为了添加自动播放,将以下代码行插入到 ProfitReport.mq5 脚本的开头,

#import "shell32.dll"
int ShellExecuteW(int hwnd,string lpOperation,string lpFile,string lpParameters,
                  string lpDirectory,int nShowCmd);
#import

最后,在 filesave() 函数的调用之后,添加

string path=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\ProfitReport.htm";
ShellExecuteW(NULL,"open",path,NULL,NULL,1);

如果在指定的变量路径中存在文件 ProfitReport.htm,则在调用函数 ShellExecuteW() 时,浏览器将打开该文件。函数 ShellExecuteW() 位于 shell32.dll 系统库中,此函数的声明被添加到该文件的开头,以提供访问。

总结

WEB 浏览器的使用允许我们同时显示很多不同的信息,这对比如组织以可视方式控制在客户端中运行的 EA 交易的单独模块的内部状态非常有用。

可以方便地同时显示资金管理数据、交易信号、跟踪止损和其他模块数据。如果必须显示很多信息,可以使用多页 HTML 报告。

应该指出,JavaScript 语言的能力比绘图还要大得多。使用此语言,我们能够创建真正的互动式 WEB 页面。您可以在互联网中找到很多包含在 WEB 页面中的现成 JavaScript 代码,以及使用该语言的各种例子。

例如,如果在客户端和浏览器之间组织了双向数据交易,则可以从浏览器窗口直接管理客户端。

我们希望本文描述的方法会有所帮助。

文件

JS_Lib.zip            - highcharts.js 和 jquery.min.js
librariesTest.zip   - highcharts.js、jquery.min.js 和 Test_01.htm
Example1.zip       - highcharts.js、jquery.min.js、Example1.htm、Example1.mq5 和 exdat.txt
Example2.zip       - Example2.htm 和 exdat.txt
Report.zip           - ProfitReport.htm 和 data.txt
ProfitReport.mq5 - 用于收集统计数据和创建 data.txt 文件的脚本