简介
本文会讲述如何生成一份 HTML 文件格式的交易结果报告(利用“EA 交易”、指标或脚本),并通过 FTP 将其上传到 WWW 服务器。我们还会考虑以短消息形式向手机发送交易事件通知。
想要更自如地阅读本文内容,建议读者熟悉 HTML (超文本标记语言)知识。
要实施报告上传,我们需要一个可以通过 FTP 接受数据的 WWW 服务器(可以是任何计算机)。而要实现接收短消息形式的交易事件通知,我们需要一个电子邮箱 - 短消息的网关(大多数移动运营商和第三方组织都提供该服务)。
1. 创建报告并通过 FTP 发送
我们来创建一个 MQL5 程序,并利用它来生成一份交易报告,再将报告通过 FTP 协议发送出去。首先,我们将其制作为脚本。将来我们能用它作为一个成品块,插入到“EA 交易”和指标中。比如说,在“EA 交易”中,您可以将此程序块作为 Trade 或 Timer 事件处理程序,于交易请求后运行此块,或是为 ChartEvent 事件设定一些动作。而在指标中,您则可以将此块加入到 Timer 或 ChartEvent 事件处理程序中。
程序创建的报告示例,如图 1、2、3 所示。或者,您也可以通过本文末尾处的链接下载该报告。
图 1. 报告示例 - 交易与仓位表
图 2. 报告示例 - 平衡图
图 3. 报告示例 - 当前工具的价格图表
交易与仓位表(图 1)中,为方便起见,所有交易都被划分成仓位。该表的左侧,会显示进入市场(持仓和添加)的交易量、时间和价格。表右侧显示的则是退出市场(部分或完全平仓)的上述参数。根据 in/out (买入/卖出),交易为分两个部分 - 一个平仓,下一个开仓。
交易与仓位表的下方是平衡图表(横轴 - 时间),而其底部则是当前工具价格图表。
该程序会在 MetaTarder5_istall_dir\MQL5\Files 文件夹中创建 "report.html"、"picture1.gif" 和 "picture2.gif" 文件(html 格式报告文件、平衡图表与价格图表的图像文件)。而且终端设置中的 FTP 发布已启用 - 它会向指定服务器发送上述三种文件。此外,我们还需要两种文件 - 带有指向持仓方向箭头的图像 - 买入或卖出("buy.gif" 与 "sell.gif")。您可以采用这些图像(下载链接在文末),也可以利用任何图形编辑器自行绘制。而这两个文件也要放到 WWW 服务器中与 "report.html" 文件相同的文件夹。
作为输入参数,该程序接受报告生成周期的开始与结束时间。本例中,报告周期的结束为当前时间,用户选择报告周期的变体:整个周期、昨天、上周、上月或去年。
简单说说我们创建报告的方式。所有可用的交易历史都会请求交易服务器。获取到的成交会被一个接一个地处理。deal_status[] 数组会存储交易是否被处理的相关信息。该数组的元素索引,就是从交易服务器交易列表中接收到的交易编号。而元素的值则如下阐释:0 - 交易尚未处理,1 - 交易已被部分处理 (in/out),127 - 交易处理完毕(其它值未被使用,留作后用)。
symb_list[] 数组包含交易得以执行的金融工具名称的列表,而 lots_list[] 数组则包含交易处理时每个工具的开仓交易量。交易量正值对应的是买入持仓,而负值则对应着卖出持仓。如果交易量等于零,则意味着该工具没有敞口仓位。如果交易处理期间出现了未于列表中(symb_list[] 数组中)出现的金融工具 - 则将其添加到那里,且金融工具的编号(symb_total 变量)以 1 为增量增长。
根据每个交易处理过程,每一个后续交易都利用相同的金融工具进行分析,直到平仓或买入/卖出。只有 deal_status[] 数组值小于 127 的那些交易会被分析。交易处理之后,对应的 deal_status[] 数组元素即被赋值为 127,如果交易为仓位 in/out,则赋值 1。如果开仓时间匹配报告周期(由 StartTime 和 EndTime 变量定义) - 则此仓位被记录到报告中(所有的输入和输出)。
除交易表外,还有一个新的当前金融工具图表被打开。此图表的所有必要属性均已提供,并用 ChartScreenShot() 函数做了一个屏幕截图 - 这样我们就能获取带有当前工具价格图表的图像文件了。接下来,此图表上的价格图表会被隐藏,余额变动图表会被绘制,然后再创建另一个屏幕截图。
两个带有图表的图像文件和带有报告的 HTML 文件创建之后,通过 FTP 发送文件的功能即被勾选。如果允许 - 则会根据 MetaTrader 5 中指定的设置,利用 SendFTP() 函数发送 "report.html"、"picture1.gif" 和 "picture2.gif" 文件。
启动 MetaQuotes Language Editor (语言编辑器),开始创建一个脚本。定义常量 - 图表刷新超时(以秒计)、价格图表的宽度和高度以及平衡图表的最大宽度。显示余额变动曲线的图表周期,根据报告周期的期限和图表的最大宽度进行选择。图表的宽度调整为平衡图所需的尺寸。
图表的高度自动计算为宽度的一半。我们还要将纵轴的宽度指定为常量 - 即图形面积相比图像宽度因纵轴而缩减的像素数。
#define timeout 10 // 图表刷新时间 #define Picture1_width 800 // 报告中图表最大宽度 #define Picture2_width 800 // 报告中价格图表宽度 #define Picture2_height 600 // 报告中价格图表高度 #define Axis_Width 59 // 纵轴宽度 (以像素为单位)
指定将从用户请求的输入参数。
// 请求输入参数 #property script_show_inputs
创建报告周期的枚举。
// 报告周期的枚举 enum report_periods { All_periods, Last_day, Last_week, Last_month, Last_year };
就报告周期询问用户(默认为整个周期)。
// 询问报告周期 input report_periods ReportPeriod=0;
编写 OnStart() 函数的主体。
void OnStart() {
确定报告周期的开头和结尾。
datetime StartTime=0; // 报告周期起始时间 datetime EndTime=TimeCurrent(); // 报告周期结束时间 // 计算报告周期起始时间 switch(ReportPeriod) { case 1: StartTime=EndTime-86400; // 日 break; case 2: StartTime=EndTime-604800; // 周 break; case 3: StartTime=EndTime-2592000; // 月 break; case 4: StartTime=EndTime-31536000; // 年 break; } // 如果不是以上的选项, 那么 StartTime=0 (整个周期)
声明将在本程序内使用的变量。变量的用途描述见评论。
int total_deals_number; // 历史数据中的交易总数 int file_handle; // 文件句柄 int i,j; // 循环计数器 int symb_total; // 交易中的资产数量 int symb_pointer; // 当前资产的指针 char deal_status[]; // 交易状态 (处理/未处理) ulong ticket; // 交易订单号 long hChart; // 图表 id double balance; // 当前余额值 double balance_prev; // 之前余额值 double lot_current; // 当前交易交易量 double lots_list[]; // 根据资产种类的开放交易量列表 double current_swap; // 当前交易的库存费 double current_profit; // 当前交易的利润 double max_val,min_val; // 最大值和最小值 string symb_list[]; // 交易的资产种类列表 string in_table_volume; // 建仓交易量 string in_table_time; // 进场时间 string in_table_price; // 进场价格 string out_table_volume; // 出场交易量 string out_table_time; // 出场时间 string out_table_price; // 出场价格 string out_table_swap; // 出场库存费 string out_table_profit; // 出场利润 bool symb_flag; // 资产在列表中的标志 datetime time_prev; // 前面的时间值 datetime time_curr; // 当前时间值 datetime position_StartTime; // 建仓时间 datetime position_EndTime; // 最后一个退场时间 ENUM_TIMEFRAMES Picture1_period; // 余额图表时段
打开一个新图表并设置其属性 - 这是一个价格图表,会在报告底部输出。
// 开启一个新图表并设置其属性 hChart=ChartOpen(Symbol(),0); ChartSetInteger(hChart,CHART_MODE,CHART_BARS); // 柱状图 ChartSetInteger(hChart,CHART_AUTOSCROLL,true); // 启用自动滚动 ChartSetInteger(hChart,CHART_COLOR_BACKGROUND,White); // 背景色为白色 ChartSetInteger(hChart,CHART_COLOR_FOREGROUND,Black); // 线和标签为黑色 ChartSetInteger(hChart,CHART_SHOW_OHLC,false); // 不显示OHLC ChartSetInteger(hChart,CHART_SHOW_BID_LINE,true); // 显示 BID 线 ChartSetInteger(hChart,CHART_SHOW_ASK_LINE,false); // 隐藏 ASK 线 ChartSetInteger(hChart,CHART_SHOW_LAST_LINE,false); // 隐藏 LAST 线 ChartSetInteger(hChart,CHART_SHOW_GRID,true); // 显示网格线 ChartSetInteger(hChart,CHART_SHOW_PERIOD_SEP,true); // 显示时段分隔符 ChartSetInteger(hChart,CHART_COLOR_GRID,LightGray); // 网格线为浅灰色 ChartSetInteger(hChart,CHART_COLOR_CHART_LINE,Black); // 图表线为黑色 ChartSetInteger(hChart,CHART_COLOR_CHART_UP,Black); // 向上的柱为黑色 ChartSetInteger(hChart,CHART_COLOR_CHART_DOWN,Black); // 向下的柱为黑色 ChartSetInteger(hChart,CHART_COLOR_BID,Gray); // BID 线为灰色 ChartSetInteger(hChart,CHART_COLOR_VOLUME,Green); // 交易量和订单水平为绿色 ChartSetInteger(hChart,CHART_COLOR_STOP_LEVEL,Red); // SL 和 TP 水平为红色 ChartSetString(hChart,CHART_COMMENT,ChartSymbol(hChart)); // 注释包含资产种类
图表截屏,并将其保存为 "picture2.gif"。
// 把图表保存为图像文件 ChartScreenShot(hChart,"picture2.gif",Picture2_width,Picture2_height);
请求现有账户整个时间段的交易历史。
// 请求整个时段的交易历史 HistorySelect(0,TimeCurrent());
打开 "report.html" 文件,并在其中写入带有报告的 HTML 页面(ANSI 编码)。
// 打开图表文件 file_handle=FileOpen("report.html",FILE_WRITE|FILE_ANSI);
编写 HTML 文档的开头部分:
- html 文档的开头 (<html>)
- 显示于您浏览器窗口顶部的标题 (<head><title>“EA 交易报告”</title></head>)
- 带有背景色的 html 文档主体部分的开头 (<body bgcolor='#EFEFEF'>)
- 居中对齐 (<center>)
- 交易与仓位表的标题 (<h2>交易报告</h2>)
- 交易与仓位表的开头,带有对齐、边框宽度、背景颜色、边框颜色、单元格间距及单元格填充 (<table align='center' border='1' bgcolor='#FFFFFF' bordercolor='#7F7FFF' cellspacing='0' cellpadding='0'>)
- 表标题
// 写 HTML 开头部分 FileWrite(file_handle,"<html>"+ "<head>"+ "<title>Expert Trade Report</title>"+ "</head>"+ "<body bgcolor='#EFEFEF'>"+ "<center>"+ "<h2>Trade Report</h2>"+ "<table align='center' border='1' bgcolor='#FFFFFF' bordercolor='#7F7FFF' cellspacing='0' cellpadding='0'>"+ "<tr>"+ "<th rowspan=2>SYMBOL</th>"+ "<th rowspan=2>Direction</th>"+ "<th colspan=3>Open</th>"+ "<th colspan=3>Close</th>"+ "<th rowspan=2>Swap</th>"+ "<th rowspan=2>Profit</th>"+ "</tr>"+ "<tr>"+ "<th>Volume</th>"+ "<th>Time</th>"+ "<th>Price</th>"+ "<th>Volume</th>"+ "<th>Time</th>"+ "<th>Price</th>"+ "</tr>");
获取列表中的交易数量。
// 历史中的交易数 total_deals_number=HistoryDealsTotal();
为 symb_list[]、lots_list[] 和 deal_status[] 数组设定维度。
// 设置资产列表,交易量列表和交易状态数组的维度 ArrayResize(symb_list,total_deals_number); ArrayResize(lots_list,total_deals_number); ArrayResize(deal_status,total_deals_number);
初始化带有 0 值的所有 deal_status[] 数组元素 - 所有交易都未处理。
// 把数组的所有原色设为0 - 交易没有被处理 ArrayInitialize(deal_status,0);
设置余额与变量的初始值,用于存储余额的前一值。
balance=0; // 初始余额 balance_prev=0; // 之前余额
设置变量的初始值,用于存储列表中的金融工具数量。
// 列表中的资产数目 symb_total=0;
创建一个顺序处理列表中每个交易的循环。
// 处理历史中的所有交易 for(i=0;i<total_deals_number;i++) {
选择当前交易并获取其订单号。
//选择交易,取得订单号 ticket=HistoryDealGetTicket(i);
利用当前交易中的利润额更改余额。
// 修改余额 balance+=HistoryDealGetDouble(ticket,DEAL_PROFIT);
获取交易时间 - 之后会频繁用到。
// 读取交易时间 time_curr=HistoryDealGetInteger(ticket,DEAL_TIME);
如果它是列表中的第一个交易 - 我们需要根据图表标绘的报告周期和区域宽度,调整报告周期的界限并选择平衡图表的周期。设置最大与最小余额的初始值(这些变量将被用于设置图表的最大与最小值)。
// 如果是第一笔交易 if(i==0) { // 如果报告时段开始早于第一笔订单 // 则报告时段将从第一笔交易开始 if(StartTime<time_curr) StartTime=time_curr; // 如果报告时段结束比当前时间晚, // 则报告时段的结束时间等于当前时间 if(EndTime>TimeCurrent()) EndTime=TimeCurrent(); // 初始化最大最小余额 // 等于当前余额 max_val=balance; min_val=balance; // 根据报告时段计算余额图表 // 时间段 Picture1_period=PERIOD_M1; if(EndTime-StartTime>(Picture1_width-Axis_Width)) Picture1_period=PERIOD_M2; if(EndTime-StartTime>(Picture1_width-Axis_Width)*120) Picture1_period=PERIOD_M3; if(EndTime-StartTime>(Picture1_width-Axis_Width)*180) Picture1_period=PERIOD_M4; if(EndTime-StartTime>(Picture1_width-Axis_Width)*240) Picture1_period=PERIOD_M5; if(EndTime-StartTime>(Picture1_width-Axis_Width)*300) Picture1_period=PERIOD_M6; if(EndTime-StartTime>(Picture1_width-Axis_Width)*360) Picture1_period=PERIOD_M10; if(EndTime-StartTime>(Picture1_width-Axis_Width)*600) Picture1_period=PERIOD_M12; if(EndTime-StartTime>(Picture1_width-Axis_Width)*720) Picture1_period=PERIOD_M15; if(EndTime-StartTime>(Picture1_width-Axis_Width)*900) Picture1_period=PERIOD_M20; if(EndTime-StartTime>(Picture1_width-Axis_Width)*1200) Picture1_period=PERIOD_M30; if(EndTime-StartTime>(Picture1_width-Axis_Width)*1800) Picture1_period=PERIOD_H1; if(EndTime-StartTime>(Picture1_width-Axis_Width)*3600) Picture1_period=PERIOD_H2; if(EndTime-StartTime>(Picture1_width-Axis_Width)*7200) Picture1_period=PERIOD_H3; if(EndTime-StartTime>(Picture1_width-Axis_Width)*10800) Picture1_period=PERIOD_H4; if(EndTime-StartTime>(Picture1_width-Axis_Width)*14400) Picture1_period=PERIOD_H6; if(EndTime-StartTime>(Picture1_width-Axis_Width)*21600) Picture1_period=PERIOD_H8; if(EndTime-StartTime>(Picture1_width-Axis_Width)*28800) Picture1_period=PERIOD_H12; if(EndTime-StartTime>(Picture1_width-Axis_Width)*43200) Picture1_period=PERIOD_D1; if(EndTime-StartTime>(Picture1_width-Axis_Width)*86400) Picture1_period=PERIOD_W1; if(EndTime-StartTime>(Picture1_width-Axis_Width)*604800) Picture1_period=PERIOD_MN1; // 改变开启的图表的时段 ChartSetSymbolPeriod(hChart,Symbol(),Picture1_period); }
如果该交易并非第一个 - 则利用标绘的余额变动图表创建 "line" 对象。只有当其(至少)一端处于报告周期内时,方可标绘此线。如果两端都在报告周期内 - 线就会“粗”。余额线是绿色的。如果余额超出了最小与最大余额的范围 - 则要对范围进行调整。
else // 如果不是第一笔交易 { // 如果交易在图表周期之内, 绘制余额线, // 并设置余额线的属性 if(time_curr>=StartTime && time_prev<=EndTime) { ObjectCreate(hChart,IntegerToString(i),OBJ_TREND,0,time_prev,balance_prev,time_curr,balance); ObjectSetInteger(hChart,IntegerToString(i),OBJPROP_COLOR,Green); // 如果线的两端都在报告周期之内, // 将是粗线 if(time_prev>=StartTime && time_curr<=EndTime) ObjectSetInteger(hChart,IntegerToString(i),OBJPROP_WIDTH,2); } // 如果余额的新值超过 // 最大值或最小值范围, 必须做调整 if(balance<min_val) min_val=balance; if(balance>max_val) max_val=balance; }
将前一个时间值赋予相应变量。
// 修改前一时间值
time_prev=time_curr;
如果交易尚未处理,则予以处理。
// 如果交易还没有被处理 if(deal_status[i]<127) {
如果此交易为余额变动且处于报告周期内,则相应的字符串会写入报告。交易被标注为已处理。
// 如果交易为余额变动 if(HistoryDealGetInteger(ticket,DEAL_TYPE)==DEAL_TYPE_BALANCE) { // 如果在报告周期内 - 在报告中写下对应描述 if(time_curr>=StartTime && time_curr<=EndTime) FileWrite(file_handle,"<tr><td colspan='9'>Balance:</td><td align='right'>",HistoryDealGetDouble(ticket,DEAL_PROFIT), "</td></tr>"); // 把交易标记为已处理 deal_status[i]=127; }
如果是买入或卖出交易,则检查列表中有没有此工具(symb_list[] 数组)。如果没有,就放上。symb_pointer 变量会指向 symb_list[] 数组中包含当前交易工具名称的元素。
// 如果交易为买入或卖出 if(HistoryDealGetInteger(ticket,DEAL_TYPE)==DEAL_TYPE_BUY || HistoryDealGetInteger(ticket,DEAL_TYPE)==DEAL_TYPE_SELL) { // 检查列表中有无此交易的资产类型 symb_flag=false; for(j=0;j<symb_total;j++) { if(symb_list[j]==HistoryDealGetString(ticket,DEAL_SYMBOL)) { symb_flag=true; symb_pointer=j; } } // 如果列表中没有此资产 if(symb_flag==false) { symb_list[symb_total]=HistoryDealGetString(ticket,DEAL_SYMBOL); lots_list[symb_total]=0; symb_pointer=symb_total; symb_total++; }
设置 position_StartTime 与 position_EndTime 变量的初始值(存储初始与最终仓位生命周期)。
// 设置交易开始时间的初始值 position_StartTime=time_curr; // 设置交易结束时间的初始值 position_EndTime=time_curr;
in_table_volume、in_table_time、in_table_price、out_table_volume、out_table_time、out_table_price、out_table_swap 和 out_table_profit 变量则会存储将加入更大表格单元格内的表格:进入市场的交易量、时间和价格;退出市场的交易量、时间、价格、掉期和利润。in_table_volume 变量还会存储金融工具的名称和到某图像的链接(对应敞口仓位方向)。为上述所有变量赋初始值。
// 创建报告中的字符串 - 金融工具, 仓位方向, 进入市场的交易量表格起始 if(HistoryDealGetInteger(ticket,DEAL_TYPE)==DEAL_TYPE_BUY) StringConcatenate(in_table_volume,"<tr><td align='left'>", symb_list[symb_pointer],"</td><td align='center'><img src='buy.gif'></td><td><table border='1' width='100%' bgcolor='#FFFFFF' bordercolor='#DFDFFF'>"); if(HistoryDealGetInteger(ticket,DEAL_TYPE)==DEAL_TYPE_SELL) StringConcatenate(in_table_volume,"<tr><td align='left'>", symb_list[symb_pointer],"</td><td align='center'><img src='sell.gif'></td><td><table border='1' width='100%' bgcolor='#FFFFFF' bordercolor='#DFDFFF'>"); // 创建进入市场的时间表格起始 in_table_time="<td><table border='1' width='100%' bgcolor='#FFFFFF' bordercolor='#DFDFFF'>"; // 创建进入市场的价格表格起始 in_table_price="<td><table border='1' width='100%' bgcolor='#FFFFFF' bordercolor='#DFDFFF'>"; // 创建退出市场的交易量表格起始 out_table_volume="<td><table border='1' width='100%' bgcolor=#FFFFFF bordercolor='#DFDFFF'>"; // 创建退出市场的时间表格起始 out_table_time="<td><table border='1' width='100%' bgcolor='#FFFFFF' bordercolor='#DFDFFF'>"; // 创建退出市场的价格表格起始 out_table_price="<td><table border='1' width='100%' bgcolor='#FFFFFF' bordercolor='#DFDFFF'>"; // 创建退出市场的库存费表格起始 out_table_swap="<td><table border='1' width='100%' bgcolor=#FFFFFF bordercolor='#DFDFFF'>"; // 创建退出市场的利润表格起始 out_table_profit="<td><table border='1' width='100%' bgcolor=#FFFFFF bordercolor='#DFDFFF'>";
处理当前仓位平仓前的所有交易。如果之前未处理,现在也都予以处理。
// 处理此仓位从现在开始的所有交易(直到平仓) for(j=i;j<total_deals_number;j++) { // 如果交易还没有被处理 - 处理它 if(deal_status[j]<127) {
选择交易并获取其订单号。
// 选择交易, 取得订单号 ticket=HistoryDealGetTicket(j);
如果交易与敞口仓位的工具相同,则予以处理。获取交易时间。如果交易时间超出了仓位时间的范围,则扩展范围。获取交易量。
// 如果交易工具和仓位工具相匹配, 则进行处理 if(symb_list[symb_pointer]==HistoryDealGetString(ticket,DEAL_SYMBOL)) { // 取得交易时间 time_curr=HistoryDealGetInteger(ticket,DEAL_TIME); // 如果交易时间超出了仓位时间范围 // - 扩展仓位时间 if(time_curr<position_StartTime) position_StartTime=time_curr; if(time_curr>position_EndTime) position_EndTime=time_curr; // 取得交易的交易量 lot_current=HistoryDealGetDouble(ticket,DEAL_VOLUME);
买入与卖出交易分别处理。先从买入交易开始。
// 如果交易是买入 if(HistoryDealGetInteger(ticket,DEAL_TYPE)==DEAL_TYPE_BUY) {
如果您已建立卖出仓位,则该买入交易就会退出市场。而如果交易量大于已建卖出持仓的交易量,则会成为 in/out。为字符串变量赋予所需值。如果交易已完全处理,则 deal_status[] 数组赋值 127;如果交易为 in/out,则赋值 1,而且必须为其它仓位分析此交易。
// 如果仓位是卖出 - 这将会从市场退出 if(NormalizeDouble(lots_list[symb_pointer],2)<0) { // 如果买入交易量大于开启的卖出持仓交易量, 这就是in/out if(NormalizeDouble(lot_current+lots_list[symb_pointer],2)>0) { // 创建退出市场的交易量表格 - 只指出开启的卖出持仓交易量 StringConcatenate(out_table_volume,out_table_volume,"<tr><td align='right'>",DoubleToString(-lots_list[symb_pointer],2),"</td></tr>"); // 把仓位标记为部分处理 deal_status[j]=1; } else { // 如果买入交易量小于等于卖出持仓 - 则是部分或全部平仓 // 创建退出市场的交易量表格 StringConcatenate(out_table_volume,out_table_volume,"<tr><td align='right'>",DoubleToString(lot_current,2),"</td></tr>"); // 把交易标记为已处理 deal_status[j]=127; } // 创建退出市场的时间表格 StringConcatenate(out_table_time,out_table_time,"<tr><td align='center'>",TimeToString(time_curr,TIME_DATE|TIME_SECONDS),"</td></tr>"); // 创建退出市场的价格表格 StringConcatenate(out_table_price,out_table_price,"<tr><td align='center'>",DoubleToString(HistoryDealGetDouble(ticket,DEAL_PRICE), (int)SymbolInfoInteger(symb_list[symb_pointer],SYMBOL_DIGITS)),"</td></tr>"); // 取得当前交易的库存费 current_swap=HistoryDealGetDouble(ticket,DEAL_SWAP); // 如果库存费等于0 - 创建退出市场库存费表格的空字符串 if(NormalizeDouble(current_swap,2)==0) StringConcatenate(out_table_swap,out_table_swap,"<tr></tr>"); // 否则创建退出市场的库存费表格的库存费字符串 else StringConcatenate(out_table_swap,out_table_swap,"<tr><td align='right'>",DoubleToString(current_swap,2),"</td></tr>"); // 取得当前交易的利润 current_profit=HistoryDealGetDouble(ticket,DEAL_PROFIT); // 如果利润为负 (亏损) - 在退出市场的利润表格中显示为红色 if(NormalizeDouble(current_profit,2)<0) StringConcatenate(out_table_profit,out_table_profit,"<tr><td align=right><SPAN style='COLOR: #EF0000'>", DoubleToString(current_profit,2),"</SPAN></td></tr>"); // 否则 - 显示为绿色 else StringConcatenate(out_table_profit,out_table_profit,"<tr><td align='right'><SPAN style='COLOR: #00EF00'>", DoubleToString(current_profit,2),"</SPAN></td></tr>"); }
如果您已建立买入持仓,此交易中的买入就会进入市场(第一个或添加)。如果与此交易对应的 deal_status[] 数组元素值为 1,则意味着完成了 in/out。为字符串变量赋予所需值,并将交易标注为已处理(为相应的 deal_status[] 数组元素赋值 127)。
else // 如果持仓为买入 - 这将是进入市场 { // 如果交易已经被部分处理 (in/out) if(deal_status[j]==1) { // 创建进入市场的交易量表格 (交易量, 是否为 in/out 之后, 写在此处) StringConcatenate(in_table_volume,in_table_volume,"<tr><td align='right'>",DoubleToString(lots_list[symb_pointer],2),"</td></tr>"); // 将会产生交易量改变的补偿, (交易的交易量已经考虑在内) lots_list[symb_pointer]-=lot_current; } // 如果交易还没有被处理 else StringConcatenate(in_table_volume,in_table_volume,"<tr><td align='right'>",DoubleToString(lot_current,2),"</td></tr>"); // 创建进入市场的时间表格 StringConcatenate(in_table_time,in_table_time,"<tr><td align center>",TimeToString(time_curr,TIME_DATE|TIME_SECONDS),"</td></tr>"); // 进入市场的价格表格 StringConcatenate(in_table_price,in_table_price,"<tr><td align='center'>",DoubleToString(HistoryDealGetDouble(ticket,DEAL_PRICE), (int)SymbolInfoInteger(symb_list[symb_pointer],SYMBOL_DIGITS)),"</td></tr>"); // 把交易标记为已处理 deal_status[j]=127; }
将仓位的交易量更改为当前交易量。如果已平仓(交易量等于零),则停止处理该仓位(利用 j 变量退出循环),并寻找下一个未处理交易(利用 i 变量进入循环)。
// 根据当前资产类型修改仓位交易量, 考虑当前交易的交易量 lots_list[symb_pointer]+=lot_current; // 如果当前资产类型的仓位交易量变成0 - 已经平仓 if(NormalizeDouble(lots_list[symb_pointer],2)==0 || deal_status[j]==1) break; }
卖出交易亦用类似方式处理,然后再利用 j 变量退出循环。
// 如果交易是卖出 if(HistoryDealGetInteger(ticket,DEAL_TYPE)==DEAL_TYPE_SELL) { // 如果仓位已经建立买入 - 这将会退出市场 if(NormalizeDouble(lots_list[symb_pointer],2)>0) { // 如果卖出交易量大于买入持仓交易量 - 这是 in/out if(NormalizeDouble(lot_current-lots_list[symb_pointer],2)>0) { // 创建退出市场的交易量表格 - 只指明买入持仓交易量 StringConcatenate(out_table_volume,out_table_volume,"<tr><td align='right'>",DoubleToString(lots_list[symb_pointer],2),"</td></tr>"); // 把仓位标记为部分处理 deal_status[j]=1; } else { // 如果卖出交易量大于开启的卖出仓位交易量 - 这是部分或全部平仓 // 创建退出市场的交易量表格 StringConcatenate(out_table_volume,out_table_volume,"<tr><td align='right'>",DoubleToString(lot_current,2),"</td></tr>"); // 把交易标记为已处理 deal_status[j]=127; } // 创建退出市场的时间表格 StringConcatenate(out_table_time,out_table_time,"<tr><td align='center'>",TimeToString(time_curr,TIME_DATE|TIME_SECONDS),"</td></tr>"); // 创建退出市场的价格表格 StringConcatenate(out_table_price,out_table_price,"<tr><td align='center'>",DoubleToString(HistoryDealGetDouble(ticket,DEAL_PRICE), (int)SymbolInfoInteger(symb_list[symb_pointer],SYMBOL_DIGITS)),"</td></tr>"); // 取得当前交易的库存费 current_swap=HistoryDealGetDouble(ticket,DEAL_SWAP); // 如果库存费等于0 - 创建退出市场库存费表格的空字符串 if(NormalizeDouble(current_swap,2)==0) StringConcatenate(out_table_swap,out_table_swap,"<tr></tr>"); // 否则创建退出市场的库存费表格的库存费字符串 else StringConcatenate(out_table_swap,out_table_swap,"<tr><td align='right'>",DoubleToString(current_swap,2),"</td></tr>"); // 取得当前交易的利润 current_profit=HistoryDealGetDouble(ticket,DEAL_PROFIT); // 如果利润为负 (亏损) - 在退出市场的利润表格中显示为红色 if(NormalizeDouble(current_profit,2)<0) StringConcatenate(out_table_profit,out_table_profit,"<tr><td align='right'> <SPAN style='COLOR: #EF0000'>",DoubleToString(current_profit,2),"</SPAN></td></tr>"); // 否则 - 显示为绿色 else StringConcatenate(out_table_profit,out_table_profit,"<tr><td align='right'><SPAN style='COLOR: #00EF00'>", DoubleToString(current_profit,2),"</SPAN></td></tr>"); } else // 如果是卖出仓位 - 这将是进入市场 { // 如果交易已经被部分处理 (in/out) if(deal_status[j]==1) { // 创建进入市场的交易量表格 (交易量, 是否为 in/out 之后, 写在此处) StringConcatenate(in_table_volume,in_table_volume,"<tr><td align='right'>",DoubleToString(-lots_list[symb_pointer],2),"</td></tr>"); // 将会产生交易量改变的补偿, (交易的交易量已经考虑在内) lots_list[symb_pointer]+=lot_current; } // 如果交易还没有被处理 else StringConcatenate(in_table_volume,in_table_volume,"<tr><td align='right'>",DoubleToString(lot_current,2),"</td></tr>"); // 创建进入市场的时间表格 StringConcatenate(in_table_time,in_table_time,"<tr><td align='center'>",TimeToString(time_curr,TIME_DATE|TIME_SECONDS),"</td></tr>"); // 进入市场的价格表格 StringConcatenate(in_table_price,in_table_price,"<tr><td align='center'>",DoubleToString(HistoryDealGetDouble(ticket,DEAL_PRICE), (int)SymbolInfoInteger(symb_list[symb_pointer],SYMBOL_DIGITS)),"</td></tr>"); // 把交易标记为已处理 deal_status[j]=127; } // 根据当前资产类型修改仓位交易量, 考虑当前交易的交易量 lots_list[symb_pointer]-=lot_current; // 如果当前资产类型的仓位交易量变成0 - 已经平仓 if(NormalizeDouble(lots_list[symb_pointer],2)==0 || deal_status[j]==1) break; } } } }
如果开仓时间(至少部分)在报告周期内,则相应的条目就会被输出到 "report.html" 文件。
// 如果仓位时间在报告时间之内- 仓位会打印到报告中 if(position_EndTime>=StartTime && position_StartTime<=EndTime) FileWrite(file_handle, in_table_volume,"</table></td>", in_table_time,"</table></td>", in_table_price,"</table></td>", out_table_volume,"</table></td>", out_table_time,"</table></td>", out_table_price,"</table></td>", out_table_swap,"</table></td>", out_table_profit,"</table></td></tr>");
为 balance_prev 变量赋予余额值。利用 i 变量退出循环。
}
// 修改余额
balance_prev=balance;
}
编写 HTML 文件的结尾(图像链接、居中对齐的结尾、主体部分的结尾、HTML 文档结尾)。关闭 "report.html" 文件。
// 创建 html-文件的末尾 FileWrite(file_handle,"</table><br><br><h2>Balance Chart</h2><img src='picture1.gif'><br><br><br><h2>Price Chart</h2><img src='picture2.gif'></center></body></html>"); // 关闭文件 FileClose(file_handle);
等待图表更新,不超过超时常量中指定的时间。
// 取得当前时间 time_curr=TimeCurrent(); // 等待图表更新 while(SeriesInfoInteger(Symbol(),Picture1_period,SERIES_BARS_COUNT)==0 && TimeCurrent()-time_curr<timeout) Sleep(1000);
设置图表的最大与最小固定值。
// 设置平衡图表的最大值和最小值 (上下边界有10% 缩进) ChartSetDouble(hChart,CHART_FIXED_MAX,max_val+(max_val-min_val)/10); ChartSetDouble(hChart,CHART_FIXED_MIN,min_val-(max_val-min_val)/10);
设置平衡图表的属性。
// 设置平衡图表的属性 ChartSetInteger(hChart,CHART_MODE,CHART_LINE); // 图表为线形图 ChartSetInteger(hChart,CHART_FOREGROUND,false); // 图表在前 ChartSetInteger(hChart,CHART_SHOW_BID_LINE,false); // 隐藏 BID 线 ChartSetInteger(hChart,CHART_COLOR_VOLUME,White); // 交易量和订单水平为白色 ChartSetInteger(hChart,CHART_COLOR_STOP_LEVEL,White); // 止损和获利水平为白色 ChartSetInteger(hChart,CHART_SHOW_GRID,true); // 显示网格 ChartSetInteger(hChart,CHART_COLOR_GRID,LightGray); // 网格为浅灰色 ChartSetInteger(hChart,CHART_SHOW_PERIOD_SEP,false); // 隐藏周期分隔 ChartSetInteger(hChart,CHART_SHOW_VOLUMES,CHART_VOLUME_HIDE); // 隐藏交易量 ChartSetInteger(hChart,CHART_COLOR_CHART_LINE,White); // 图表为白色 ChartSetInteger(hChart,CHART_SCALE,0); // 最小尺度 ChartSetInteger(hChart,CHART_SCALEFIX,true); // 纵轴固定尺度 ChartSetInteger(hChart,CHART_SHIFT,false); // 没有图表转换 ChartSetInteger(hChart,CHART_AUTOSCROLL,true); // 启用自动滚动 ChartSetString(hChart,CHART_COMMENT,"BALANCE"); // 图表的注释
重绘平衡图表。
// 重绘平衡图表 ChartRedraw(hChart); Sleep(8000);
图表截图(保存 "picture1.gif" 图像)。图表宽度会适应报告周期宽度调整(但由于假期的原因,误差频频出现,图表也变得比余额变动曲线宽),高度按宽度的一半计算。
// 平衡图表截图 ChartScreenShot(hChart,"picture1.gif",(int)(EndTime-StartTime)/PeriodSeconds(Picture1_period), (int)(EndTime-StartTime)/PeriodSeconds(Picture1_period)/2,ALIGN_RIGHT);
删除图表的所有对象并关闭。
// 删除平衡图表的所有对象 ObjectsDeleteAll(hChart); // 关闭图表 ChartClose(hChart);
如果允许通过 FTP 发送文件,则发送下述三个文件:"report.html"、picture1.gif "和" picture2.gif "。
// 如果启用了发布报告 - 通过 FTP 发送 // HTML-文件和两个图片 - 价格图表和平衡图表 if(TerminalInfoInteger(TERMINAL_FTP_ENABLED)) { SendFTP("report.html"); SendFTP("picture1.gif"); SendFTP("picture2.gif"); } }
程序描述到此为止。要通过 FTP 发送文件,您必须调整 MetaTrader 5 设置 - 前往 Tools 菜单,然后是 Options,再打开 Publisher 选项卡(图 4)。
图 4. 通过 FTP 发布报告的选项
在 Options 对话框中,您需要勾选 "Enable" 项,指定账号、FTP 地址、路径、登录名及访问密码。周期性刷新没有影响。
现在,您可以运行脚本。运行后,屏幕上会出现平衡图表,持续几秒后消失。您可以在日志中查找潜在的错误, 看看文件是否已通过 FTP 发送。如果一切正常,则服务器指定文件夹中就会出现三个新文件。如果您把两个带有箭头图像的文件放到那里,WWW 服务器已完成配置且正常运行,则您可以通过网页浏览器打开报告。
2. 以短消息形式向手机发送通知
有些时候,您会离开自己的计算机和其它电子设备,身边只有一部手机。但您却想管理您账户的相关交易,或是监控金融工具的报价。这种情况下,您就可以设置通过短消息向手机发送通知。许多移动运营商(及相关第三方)都提供电子邮件-短信息服务,允许您以信件形式接收发送到某特定电邮地址的消息。
为此,您必须有一个电子邮箱(特别是,您必须知道自己的 SMTP 服务器)。调整您的 MetaTrader 5 设置 - 前往 Tools 菜单,然后是 Options,再打开 Email 选项卡(图 5)。
图 5. 通过电邮发送通知的相关设置
勾选 "Enable" 项,指定 SMTP 服务器地址、登录名与密码、发件人地址 (您的电邮)和收件人地址 - 用于发送短消息的电邮地址(详询您的移动运营商)。如果一切正常,那么,只要您点击 "Test" 按钮,就会发送一条验证信息(详见“日志”)。
如果价格达到某特定水平,最简单的通知方式就是创建一个提醒。为此,打开相应的 "Toolbox" 选项卡,鼠标右击并选择 "Create" (图 6)。
图 6. 创建提醒
于该窗口中勾选 "Enable" 项,选择 "Mail" 动作,选取金融工具、条件,输入条件值并编写消息文本。如果您不想消息重复出现,则在 "Maximum iterations" (最大迭代)中输入 1。填妥所有字段后,点击 OK。
如果我们通过一个 MQL5 程序发送消息,则有更多的可能性。我们会采用 SendMail() 函数。它有两个参数。第一个是标题,第二个则是消息主体。
您可以在交易请求(OrderSend() 函数)后或 Trade 事件处理程序中调用 SendMail() 函数。这样我们就有了下述交易事件的通知 - 进入市场、下订单、平仓。或者,您也可以将 SendMail() 置入 OnTimer() 函数 - 我们就会收到有关当前报价的周期性通知。如有特定交易信号出现 - 指标线相交、价格达到某些线或水平时 - 您就可以安排发送通知了。
我们举几个例子。
在“EA 交易”或脚本中,如果您替换掉
OrderSend(request,result};
换上
string msg_subj,msg_text; if(OrderSend(request,result)) { switch(request.action) { case TRADE_ACTION_DEAL: switch(request.type) { case ORDER_TYPE_BUY: StringConcatenate(msg_text,"Buy ",result.volume," ",request.symbol," 价位 ",result.price,", 止损=",request.sl,", 获利=",request.tp); break; case ORDER_TYPE_SELL: StringConcatenate(msg_text,"Sell ",result.volume," ",request.symbol," 价位 ",result.price,", 止损=",request.sl,", 获利=",request.tp); break; } break; case TRADE_ACTION_PENDING: switch(request.type) { case ORDER_TYPE_BUY_LIMIT: StringConcatenate(msg_text,"设置BuyLimit ",result.volume," ",request.symbol," 价位 ",request.price,", 止损=",request.sl,", 获利=",request.tp); break; case ORDER_TYPE_SELL_LIMIT: StringConcatenate(msg_text,"设置 SellLimit ",result.volume," ",request.symbol," 价位 ",request.price,", 止损=",request.sl,", 获利=",request.tp); break; case ORDER_TYPE_BUY_STOP: StringConcatenate(msg_text,"设置 BuyStop ",result.volume," ",request.symbol," 价位 ",request.price,", 止损=",request.sl,", 获利=",request.tp); break; case ORDER_TYPE_SELL_STOP: StringConcatenate(msg_text,"设置 SellStop ",result.volume," ",request.symbol," 价位 ",request.price,", 止损=",request.sl,", 获利=",request.tp); break; case ORDER_TYPE_BUY_STOP_LIMIT: StringConcatenate(msg_text,"设置 BuyStopLimit ",result.volume," ",request.symbol," 价位 ",request.price,", stoplimit=",request.stoplimit, ", 止损=",request.sl,", 获利=",request.tp); break; case ORDER_TYPE_SELL_STOP_LIMIT: StringConcatenate(msg_text,"设置 SellStop ",result.volume," ",request.symbol," 价位 ",request.price,", stoplimit=",request.stoplimit, ", 止损=",request.sl,", 获利=",request.tp); break; } break; case TRADE_ACTION_SLTP: StringConcatenate(msg_text,"修改止损和获利. SL=",request.sl,", TP=",request.tp); break; case TRADE_ACTION_MODIFY: StringConcatenate(msg_text,"修改订单",result.price,", 止损=",request.sl,", 获利=",request.tp); break; case TRADE_ACTION_REMOVE: msg_text="删除订单"; break; } } else msg_text="错误!"; StringConcatenate(msg_subj,AccountInfoInteger(ACCOUNT_LOGIN),"-",AccountInfoString(ACCOUNT_COMPANY)); SendMail(msg_subj,msg_text);
则 OrderSend() 函数会在交易请求后,利用 SendMail() 函数发送一条消息。其中包含交易账号、经纪人名称以及实施动作(买入、卖出、下达挂单、订单修改或删除)的相关信息,如下所示:
59181-MetaQuotes Software Corp. Buy 0.1 EURUSD at price 1.23809, SL=1.2345, TP=1.2415
而且,如果是在任何“EA 交易”或 OnInit() 的主体内的指标中,您就要利用 EventSetTimer() 函数启动计时器 (它只有一个参数 - 以秒计的计时器周期):
void OnInit() { EventSetTimer(3600); }
在 OnDeinit() 中,不要忘了用 EventKillTimer() 将其关闭:
void OnDeinit(const int reason) { EventKillTimer(); }
而在 OnTimer() 中,则是利用 SendMail() 发送消息:
void OnTimer() { SendMail(Symbol(),DoubleToString(SymbolInfoDouble(Symbol(),SYMBOL_BID),_Digits)); }
之后,您就会收到带有指定周期的当前金融工具价格的相关消息。
总结
本文讲述的是,如何利用 MQL5 程序创建一个 HTML 与图像文件,以及如何通过 FTP 将其上传到 WWW 服务器。还介绍了以短消息形式向您的手机发送通知的配置方式。