交易报告及短信通知的创建和发布(基础篇)
把交易报告推到手机上
MT5 客户端本身不直接提供把每日交易报告转发到外部短信网关的现成按钮,但借助 MQL5 的邮件与 WebRequest 接口,可以在 EA 或脚本里把持仓、盈亏、净值打包成文本发出去。 早期社区里就有作者在 2013-12-03 公开的示例被查阅过 2629 次、收到 4 条讨论,说明这类「报告外发」需求在实盘群体里一直有存量。 实现思路不复杂:先用 HistorySelect 把指定周期订单捞出来,拼成字符串,再用 SendMail 或 POST 到第三方接口。外汇与贵金属杠杆高,报告外发只解决通知问题,不替代风控。
◍ 先把运行环境备齐
这套方案的核心动作只有两件:把 MT5 里跑出来的交易结果生成为 HTML 报告,再通过 FTP 推到 WWW 服务器;同时把交易事件以短消息发到手机。前者依赖一台能收 FTP 的服务器,后者依赖一个邮箱转短信网关。 读这段代码前,最好你本来就懂一点 HTML 标签结构,否则报告模板改起来会卡在字符串拼接上。MT5 端不帮你排版,只负责把内容塞进你写的标签里。 实测环境门槛很低:任何能跑 FTP 服务的机器都行,不一定非得公网服务器;短信网关大多数移动运营商和第三方都提供,费率按条计,做事件通知够用。外汇与贵金属交易本身高风险,自动化报告只是降低信息延迟,不改善胜率。
「用脚本把交易历史打包成 FTP 报告」
在 MT5 里把成交记录自动汇成网页报告并发到远程服务器,最省事的做法是先写成一个脚本,后续可整块塞进 EA 的 Trade/Timer 事件或指标的 ChartEvent 里。脚本跑完会在 MQL5\Files 目录下落三个文件:report.html、picture1.gif(余额曲线)、picture2.gif(当前品种价格图),再靠终端 Publisher 里的 FTP 发布把这三样推上去。 处理交易历史时,程序先把全部 deal 拉回来,用 deal_status[] 按交易号标记状态:0 未处理、1 部分处理(in/out)、127 已完毕。symb_list[] 存涉及品种名,lots_list[] 存对应敞口手数,正为买负为卖,零表示无仓。遇到新品种就追加进列表,symb_total 加一。 报告里的仓位表把每笔成交按开平拆左右两栏,仅当开仓时间落在 StartTime~EndTime 内才写进 html。价格图用 ChartScreenShot() 截完隐藏,再画余额变动线截第二张图;线宽随是否两端都在周期内变化,绿色表示余额,越界就动态扩 min/max 范围。 FTP 发送靠 SendFTP() 且依赖终端设置:Tools→Options→Publisher 勾 Enable,填账号、地址、路径、登录密码。脚本运行后平衡图闪几秒即删,日志里能查是否上传成功,正常的话服务器目录多三个文件,再把 buy.gif / sell.gif 箭头图放同目录就能浏览器看报告。 下面这段是脚本头部常量和周期枚举的原文,定义了图表超时 10 秒、两张图最大宽 800、价格图高 600、纵轴占 59 像素,以及全部/昨/上周/上月/去年五种周期选择。
class="macro">#define timeout class="num">10 class=class="str">"cmt">// 图表刷新时间 class="macro">#define Picture1_width class="num">800 class=class="str">"cmt">// 报告中图表最大宽度 class="macro">#define Picture2_width class="num">800 class=class="str">"cmt">// 报告中价格图表宽度 class="macro">#define Picture2_height class="num">600 class=class="str">"cmt">// 报告中价格图表高度 class="macro">#define Axis_Width class="num">59 class=class="str">"cmt">// 纵轴宽度 (以像素为单位) class=class="str">"cmt">// 请求输入参数 class="macro">#class="kw">property script_show_inputs class=class="str">"cmt">// 报告周期的枚举 enum report_periods { All_periods, Last_day, Last_week, Last_month, Last_year }; class=class="str">"cmt">// 询问报告周期 class="kw">input report_periods ReportPeriod=class="num">0; class="type">void OnStart() { class="type">class="kw">datetime StartTime=class="num">0; class=class="str">"cmt">// 报告周期起始时间 class="type">class="kw">datetime EndTime=TimeCurrent(); class=class="str">"cmt">// 报告周期结束时间 class=class="str">"cmt">// 计算报告周期起始时间 class="kw">switch(ReportPeriod) { case class="num">1:
时间窗口与图表对象的变量布局
这段逻辑先用 switch 把周期选项映射成秒数:日减 86400、周减 604800、月减 2592000、年减 31536000,若都不匹配则 StartTime 置 0,等于拉取全部历史。做回测或账户体检时,改这几个常数就能直接切换统计粒度,不用动后续结构。 紧接着是一批变量声明,覆盖交易枚举、资产列表、进出场明细与图表句柄。注意 deal_status[] 用 char 标记处理状态,lots_list[] 和 symb_list[] 成对出现,说明后面会按资产种类归并持仓量,而不是混在一起算。 图表初始化部分用 ChartOpen 拿句柄后,连刷十几条 ChartSetInteger。背景白、前景黑、不显示 OHLC、只留 BID 线,网格浅灰且开启时段分隔——这种配色在 MT5 里加载大量历史柱时更不容易看花眼。外汇和贵金属杠杆高,历史样本若跨年,先确认 StartTime 映射没溢出再跑。
StartTime=EndTime-class="num">86400; class=class="str">"cmt">// 日 break; case class="num">2: StartTime=EndTime-class="num">604800; class=class="str">"cmt">// 周 break; case class="num">3: StartTime=EndTime-class="num">2592000; class=class="str">"cmt">// 月 break; case class="num">4: StartTime=EndTime-class="num">31536000;class=class="str">"cmt">// 年 break; } class=class="str">"cmt">// 如果不是以上的选项, 那么 StartTime=class="num">0 (整个周期) class="type">int total_deals_number; class=class="str">"cmt">// 历史数据中的交易总数 class="type">int file_handle; class=class="str">"cmt">// 文件句柄 class="type">int i,j; class=class="str">"cmt">// 循环计数器 class="type">int symb_total; class=class="str">"cmt">// 交易中的资产数量 class="type">int symb_pointer; class=class="str">"cmt">// 当前资产的指针 class="type">char deal_status[]; class=class="str">"cmt">// 交易状态 (处理/未处理) class="type">class="kw">ulong ticket; class=class="str">"cmt">// 交易订单号 class="type">long hChart; class=class="str">"cmt">// 图表 id class="type">class="kw">double balance; class=class="str">"cmt">// 当前余额值 class="type">class="kw">double balance_prev; class=class="str">"cmt">// 之前余额值 class="type">class="kw">double lot_current; class=class="str">"cmt">// 当前交易交易量 class="type">class="kw">double lots_list[]; class=class="str">"cmt">// 根据资产种类的开放交易量列表 class="type">class="kw">double current_swap; class=class="str">"cmt">// 当前交易的库存费 class="type">class="kw">double current_profit; class=class="str">"cmt">// 当前交易的利润 class="type">class="kw">double max_val,min_val; class=class="str">"cmt">// 最大值和最小值 class="type">class="kw">string symb_list[]; class=class="str">"cmt">// 交易的资产种类列表 class="type">class="kw">string in_table_volume; class=class="str">"cmt">// 建仓交易量 class="type">class="kw">string in_table_time; class=class="str">"cmt">// 进场时间 class="type">class="kw">string in_table_price; class=class="str">"cmt">// 进场价格 class="type">class="kw">string out_table_volume; class=class="str">"cmt">// 出场交易量 class="type">class="kw">string out_table_time; class=class="str">"cmt">// 出场时间 class="type">class="kw">string out_table_price; class=class="str">"cmt">// 出场价格 class="type">class="kw">string out_table_swap; class=class="str">"cmt">// 出场库存费 class="type">class="kw">string out_table_profit; class=class="str">"cmt">// 出场利润 class="type">bool symb_flag; class=class="str">"cmt">// 资产在列表中的标志 class="type">class="kw">datetime time_prev; class=class="str">"cmt">// 前面的时间值 class="type">class="kw">datetime time_curr; class=class="str">"cmt">// 当前时间值 class="type">class="kw">datetime position_StartTime; class=class="str">"cmt">// 建仓时间 class="type">class="kw">datetime position_EndTime; class=class="str">"cmt">// 最后一个退场时间 ENUM_TIMEFRAMES Picture1_period; class=class="str">"cmt">// 余额图表时段 class=class="str">"cmt">// 开启一个新图表并设置其属性 hChart=ChartOpen(Symbol(),class="num">0); ChartSetInteger(hChart,CHART_MODE,CHART_BARS); class=class="str">"cmt">// 柱状图 ChartSetInteger(hChart,CHART_AUTOSCROLL,true); class=class="str">"cmt">// 启用自动滚动 ChartSetInteger(hChart,CHART_COLOR_BACKGROUND,White); class=class="str">"cmt">// 背景色为白色 ChartSetInteger(hChart,CHART_COLOR_FOREGROUND,Black); class=class="str">"cmt">// 线和标签为黑色 ChartSetInteger(hChart,CHART_SHOW_OHLC,false); class=class="str">"cmt">// 不显示OHLC ChartSetInteger(hChart,CHART_SHOW_BID_LINE,true); class=class="str">"cmt">// 显示 BID 线 ChartSetInteger(hChart,CHART_SHOW_ASK_LINE,false); class=class="str">"cmt">// 隐藏 ASK 线 ChartSetInteger(hChart,CHART_SHOW_LAST_LINE,false); class=class="str">"cmt">// 隐藏 LAST 线 ChartSetInteger(hChart,CHART_SHOW_GRID,true); class=class="str">"cmt">// 显示网格线 ChartSetInteger(hChart,CHART_SHOW_PERIOD_SEP,true); class=class="str">"cmt">// 显示时段分隔符 ChartSetInteger(hChart,CHART_COLOR_GRID,LightGray); class=class="str">"cmt">// 网格线为浅灰色 ChartSetInteger(hChart,CHART_COLOR_CHART_LINE,Black); class=class="str">"cmt">// 图表线为黑色 ChartSetInteger(hChart,CHART_COLOR_CHART_UP,Black); class=class="str">"cmt">// 向上的柱为黑色
◍ 把图表配色和成交记录导成报告文件
用 ChartSetInteger 可以批量改图表视觉属性,下面这段把向下 K 线设成黑色、BID 线灰色、成交量与挂单水平绿色、止损止盈水平红色,并在图表注释里写入当前品种名。改完配色后调用 ChartScreenShot 把图存成 picture2.gif,宽高由 Picture2_width / Picture2_height 控制,方便后续贴进报告。 HistorySelect(0, TimeCurrent()) 拉取从 0 到当前时间的全部历史成交,这是生成交易报表的前提。随后 FileOpen 以 FILE_WRITE|FILE_ANSI 打开 report.html,用 FileWrite 写 HTML 头,包括标题、灰底页面、居中表格,表头跨行跨列定义了 SYMBOL、Direction、Open(3 列)、Close(3 列)、Swap、Profit。 这套写法直接复制进 EA 的 OnDeinit 或脚本里就能跑,开 MT5 新建脚本粘贴,把 Picture2_width 设成 800、Picture2_height 设成 600,编译后拖到黄金 XAUUSD 图表上,可能在 MQL5/Files 目录得到带图表的 report.html。外汇与贵金属杠杆高,历史报表仅反映过去,不预示后续盈亏。
ChartSetInteger(hChart,CHART_COLOR_CHART_DOWN,Black); class=class="str">"cmt">// 向下的柱为黑色 ChartSetInteger(hChart,CHART_COLOR_BID,Gray); class=class="str">"cmt">// BID 线为灰色 ChartSetInteger(hChart,CHART_COLOR_VOLUME,Green); class=class="str">"cmt">// 交易量和订单水平为绿色 ChartSetInteger(hChart,CHART_COLOR_STOP_LEVEL,Red); class=class="str">"cmt">// SL 和 TP 水平为红色 ChartSetString(hChart,CHART_COMMENT,ChartSymbol(hChart)); class=class="str">"cmt">// 注释包含资产种类 class=class="str">"cmt">// 把图表保存为图像文件 ChartScreenShot(hChart,"picture2.gif",Picture2_width,Picture2_height); class=class="str">"cmt">// 请求整个时段的交易历史 HistorySelect(class="num">0,TimeCurrent()); class=class="str">"cmt">// 打开图表文件 file_handle=FileOpen("report.html",FILE_WRITE|FILE_ANSI); class=class="str">"cmt">// 写 HTML 开头部分 FileWrite(file_handle,"<html>"+ "<head>"+ "<title>Expert Trade Report</title>"+ "</head>"+ "<body bgcolor=&class="macro">#x27;class="macro">#EFEFEF&class="macro">#x27;>"+ "<center>"+ "<h2>Trade Report</h2>"+ "<table align=&class="macro">#x27;center&class="macro">#x27; border=&class="macro">#x27;class="num">1&class="macro">#x27; bgcolor=&class="macro">#x27;class="macro">#FFFFFF&class="macro">#x27; bordercolor=&class="macro">#x27;class="macro">#7F7FFF&class="macro">#x27; cellspacing=&class="macro">#x27;class="num">0&class="macro">#x27; cellpadding=&class="macro">#x27;class="num">0&class="macro">#x27;>"+ "<tr>"+ "<th rowspan=class="num">2>SYMBOL</th>"+ "<th rowspan=class="num">2>Direction</th>"+ "<th colspan=class="num">3>Open</th>"+ "<th colspan=class="num">3>Close</th>"+ "<th rowspan=class="num">2>Swap</th>"+ "<th rowspan=class="num">2>Profit</th>"+ "</tr>"+ "<tr>"+ "<th>Volume</th>"+
「按回测跨度自动切换余额曲线周期」
这段逻辑解决一个很实际的问题:历史成交跨度从几分钟到几十天都有可能,如果统一用 M1 画余额曲线,图表宽度根本塞不下。代码里用 EndTime-StartTime 与 (Picture1_width-Axis_Width) 的倍数关系,逐级把周期从 PERIOD_M1 往上抬到 PERIOD_H3。 具体看那串 if:当总跨度超过图表可画像素宽度的 120 倍就切 M3,超过 1800 倍切 H1,超过 7200 倍才用 H3。也就是说,若你图表绘图区宽 800 像素、轴占 100,那么跨度大于 700×1800=1,260,000 秒(约 14.6 天)才会落到 H3,否则更细的周期优先。 首笔成交时还会顺手把 StartTime 钳到第一笔 deal 的时间、EndTime 钳到 TimeCurrent(),避免空段。外汇与贵金属杠杆品种历史回测区间长、跳空多,这样动态选周期能让资金曲线在 MT5 里不至于被压成一条竖线。
"<th>Time</th>"+
"<th>Price</th>"+
"<th>Volume</th>"+
"<th>Time</th>"+
"<th>Price</th>"+
"</tr>");
class=class="str">"cmt">// 历史中的交易数
total_deals_number=HistoryDealsTotal();
class=class="str">"cmt">// 设置资产列表,交易量列表和交易状态数组的维度
ArrayResize(symb_list,total_deals_number);
ArrayResize(lots_list,total_deals_number);
ArrayResize(deal_status,total_deals_number);
class=class="str">"cmt">// 把数组的所有原色设为0 - 交易没有被处理
ArrayInitialize(deal_status,class="num">0);
balance=class="num">0; class=class="str">"cmt">// 初始余额
balance_prev=class="num">0; class=class="str">"cmt">// 之前余额
class=class="str">"cmt">// 列表中的资产数目
symb_total=class="num">0;
class=class="str">"cmt">// 处理历史中的所有交易
for(i=class="num">0;i<total_deals_number;i++)
{
class=class="str">"cmt">//选择交易,取得订单号
ticket=HistoryDealGetTicket(i);
class=class="str">"cmt">// 修改余额
balance+=HistoryDealGetDouble(ticket,DEAL_PROFIT);
class=class="str">"cmt">// 读取交易时间
time_curr=HistoryDealGetInteger(ticket,DEAL_TIME);
class=class="str">"cmt">// 如果是第一笔交易
if(i==class="num">0)
{
class=class="str">"cmt">// 如果报告时段开始早于第一笔订单
class=class="str">"cmt">// 则报告时段将从第一笔交易开始
if(StartTime<time_curr) StartTime=time_curr;
class=class="str">"cmt">// 如果报告时段结束比当前时间晚,
class=class="str">"cmt">// 则报告时段的结束时间等于当前时间
if(EndTime>TimeCurrent()) EndTime=TimeCurrent();
class=class="str">"cmt">// 初始化最大最小余额
class=class="str">"cmt">// 等于当前余额
max_val=balance;
min_val=balance;
class=class="str">"cmt">// 根据报告时段计算余额图表
class=class="str">"cmt">// 时间段
Picture1_period=PERIOD_M1;
if(EndTime-StartTime>(Picture1_width-Axis_Width)) Picture1_period=PERIOD_M2;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">120) Picture1_period=PERIOD_M3;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">180) Picture1_period=PERIOD_M4;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">240) Picture1_period=PERIOD_M5;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">300) Picture1_period=PERIOD_M6;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">360) Picture1_period=PERIOD_M10;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">600) Picture1_period=PERIOD_M12;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">720) Picture1_period=PERIOD_M15;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">900) Picture1_period=PERIOD_M20;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">1200) Picture1_period=PERIOD_M30;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">1800) Picture1_period=PERIOD_H1;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">3600) Picture1_period=PERIOD_H2;
if(EndTime-StartTime>(Picture1_width-Axis_Width)*class="num">7200) Picture1_period=PERIOD_H3;