运用 MQL5 和 MQL4 开发品种选择和导航实用程序
概述
经验丰富的交易者非常清楚交易中最劳神的事情并非开单和跟踪持仓,而是选择交易品种并寻找入场点。
当然,如果您只关注 1-2 个品种,那么这都不是很大的问题。 但如果您的交易工具涵盖数百支股票和数十种外汇交易品种,则可能需要几个小时才能发现合适的入场点。
在本文中,我们将开发一款简化搜索股票的 EA。 EA 将以三种方式提供帮助:
- 它预先过滤股票,为我们提供符合条件的清单;
- 通过生成的股票清单能够简化导航;
- 它能显示用于制定决策的其它必要数据。
初始 EA 模板
最初,我们打算运用 MQL5 开发 EA。 然而,由于众多经纪商仍然未提供 MetaTrader 5 账户,我们只得在文章末尾重新开发 EA,以便它也可以在 MetaTrader 4 中使用。
因此,我们先来准备一个模板,它与 MQL5 向导所生成的模板几乎没有区别:
//+------------------------------------------------------------------+ //| _finder.mq5 | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Klymenko Roman (needtome@icloud.com)" #property link "https://logmy.net" #property version "1.00" #property strict //+------------------------------------------------------------------+ //| 智能系统初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 创建定时器 EventSetTimer(1); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| 智能系统逆初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+ //| 定时器函数 | //+------------------------------------------------------------------+ void OnTimer() { } //+------------------------------------------------------------------+ void OnChartEvent(const int id, // 事件 ID const long& lparam, // 长整数型事件参数 const double& dparam, // 双精度型事件参数 const string& sparam) // 字符串型事件参数 { }
在此模板中,我们在创建 EA 伊始注册定时器。 我们的定时器每秒激活一次。 这意味着每秒调用一次 OnTimer 标准函数。
与 MQL5 向导生成的典型模板不同的唯一字符串是 #property strict。 此字符串是 EA 在 MetaTrader 4 中正常运行所必需的。 由于它对 MetaTrader 5 没有显著影响,我们会提前将其添加到我们的模板中。
我们将应用以下标准函数:
- OnInit: 在图表上显示符合我们条件的交易产品按钮;
- OnDeinit: 删除定时器和 EA 创建的所有图形对象;
- OnTimer: 定时器用于判断 EA 在图表上所创建图形对象的点击次数;
- OnChartEvent: EA 在启动图表上所创建图形对象的点击事件响应。
满足我们条件的品种列表将存储在 CArrayString 类型对象中。 所以,将含有对象描述的 MQH 文件 嵌入 EA 中:
#include <Arrays\ArrayString.mqh>
我们在处理图表时还需要 Chart 类型的对象。 我们也要包含它的定义:
#include <Charts\Chart.mqh>
所有这些都应在我们的模板开头,在 #property 字符串块之后,在任何函数之外完成的。
接下来,我们需要决定 EA 创建的所有按钮的宽度和高度。 将这些数值设置为常量,在 #include 字符串块之后指定它们:
#define BTN_HEIGHT (20) #define BTN_WIDTH (100)
输入
EA 将遵照输入进行管控。 我们看看它们,以便我们可以立即定义我们将在本文后面实现的函数:
sinput string delimeter_01=""; // --- 过滤设置 --- input bool noSYMBmarketWath=true; // 是否不在市场观察中隐藏 input bool noSYMBwithPOS=true; // 若有持仓则隐藏 input ValueOfSpread hide_SPREAD=spread_b1; // 在点差情况下隐藏 input uint hide_PRICE_HIGH=0; // 若价格太高则隐藏 input uint hide_PRICE_LOW=0; // 若价格太低则隐藏 input bool hideProhibites=true; // 若禁止交易则隐藏 input bool hideClosed=true; // 若市场收市则隐藏 input StartHour hide_HOURS=hour_any; // 若有开放时间则显示 input double hideATRcents=0.00; // 如果 ATR 小于设定的美元值则隐藏 sinput string delimeter_02=""; // --- 图表设置 --- input bool addInfoWatch=false; // 将图表添加到市场观察中 input bool viewCandle=true; // 打开烛条图表 input bool viewVolumes=true; // 显示逐笔报价交易量 input bool showInfoSymbol=true; // 显示走势方向 input bool showNameSymbol=true; // 显示品名
我们可以立即注意到有两个输入为自定义类型。 因此,我们在输入之前添加这些类型的定义。 两种自定义类型都是枚举。
ValueOfSpread 枚举定义 EA 所要显示的品种的点差值条件:
enum ValueOfSpread { spread_no,//无 spread_b05,// > 0.05% spread_b1,// > 0.1% spread_b15,// > 0.15% spread_l15,// < 0.15% spread_l1,// < 0.1% };
超过价格 0.1% 的点差被视为增加。 因此,我们默认只显示点差小于 0.1% 的品种。 因此,Hide in case of a spread 参数值为 > 0.1%。 但是,如果您的经纪商提供的此类品种列表太小,您可以为此参数选择其它数值。
StartHour 枚举包含主要周期的列表,一些市场会在其间开放:
enum StartHour { hour_any, //任何时间 hour_9am, // 9 am hour_10am,// 10 am hour_4pm, // 4 pm hour_0am, // 午夜 };
9 am (或是任何其它数值) 并不意味着只显示在指定时间开单的品种。 代之,它意味着显示此时(例如,9:05)要开单的品种。
相应地,4 pm 意味着仅显示在 16:30 开放的美国股市股票。
10 am 主要涉及俄罗斯和欧洲股市股票。
9 am 是一些指数的开放时间。
最后, midnight 是一个外汇市场开放时间,因为它全天候工作。
全局变量
在我们开始运用标准函数的内容之前,我们只需要声明一系列变量,它们要在我们的整个 EA 范围内可见。 我们在输入后添加它们:
// 在 EA 创建的所有图形对象名称中添加的前缀: string exprefix="finder"; // 符合我们条件的品种数组: CArrayString arrPanel1; // arrPanel1 数组中当前品种的索引: int panel1val; // 用于存储 EA 创建的图表的数组(此刻此处只有一个图表): CChart charts[]; // 用于存储指向 EA 创建的图表的指针的数组(此刻此处只有一个指针): long curChartID[];
澄清我们为什么需要这些变量的注释。
所有准备工作都已就绪。 我们现在准备开始开发 EA。 但首先,我们来看看结果:
//+------------------------------------------------------------------+ //| _finder.mq5 | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Klymenko Roman (needtome@icloud.com)" #property link "https://logmy.net" #property version "1.00" #property strict #include <Arrays\ArrayString.mqh> #include <Charts\Chart.mqh> #define BTN_HEIGHT (20) #define BTN_WIDTH (100) enum ValueOfSpread { spread_no,//无 spread_b05,// > 0.05 % spread_b1,// > 0.1 % spread_b15,// > 0.15 % spread_l15,// < 0.15 % spread_l1,// < 0.1 % }; enum StartHour { hour_any,//任何时间 hour_9am,// 9 am hour_10am,// 10 am hour_4pm,// 4 pm hour_0am,// 午夜 }; input bool noSYMBmarketWath=true; // 在市场观察中隐藏品种< input bool noSYMBwithPOS=true; // 若有持仓则隐藏 input ValueOfSpread hide_SPREAD=spread_b1; // 隐藏有点差品种 input uint hide_PRICE_HIGH=0; // 隐藏价格太高品种 (0 - 不隐藏) input uint hide_PRICE_LOW=0; // 隐藏价格太低品种 (0 - 不隐藏) input bool hideProhibites=true; // 隐藏不可交易品种 input StartHour hide_HOURS=hour_any; // 显示仅在某时刻开放的品种 input bool viewCandle=true; // 打开烛条图表 // 在 EA 创建的所有图形对象名称中添加的前缀: string exprefix="finder"; // 符合我们条件的品种数组: CArrayString arrPanel1; // arrPanel1 数组中当前品种的索引: int panel1val; // 用于存储 EA 创建的图表的数组(此刻此处只有一个图表): CChart charts[]; // 用于存储指向 EA 创建的图表的指针的数组(此刻此处只有一个指针): long curChartID[]; //+------------------------------------------------------------------+ //| 智能系统初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 创建定时器 EventSetTimer(1); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| 智能系统逆初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(reason!=REASON_CHARTCHANGE){ ObjectsDeleteAll(0, exprefix); } EventKillTimer(); } //+------------------------------------------------------------------+ //| 定时器函数 | //+------------------------------------------------------------------+ void OnTimer() { } void OnChartEvent(const int id, // 事件 ID const long& lparam, // 长整数型事件参数 const double& dparam, // 双精度型事件参数 const string& sparam) // 字符串型事件参数 { }
品种过滤函数
我们从显示满足我们条件的品种按钮的函数开始。 我们来命名这个函数 start_symbols。 函数会在 OnInit 函数当中调用。 结果就是 OnInit 函数采用的最终形式:
//+------------------------------------------------------------------+ //| 智能系统初始化函数 | //+------------------------------------------------------------------+ int OnInit() { start_symbols(); //--- 创建定时器 EventSetTimer(1); //--- return(INIT_SUCCEEDED); }
如果所有内容都可以在 OnInit 中实现,为什么我们需要一个单独的函数? 一切都很简单。 我们不仅在启动 EA 时调用此函数,并且在按 R 键时亦调用此函数。 因此,我们能够轻松更新品种列表,而无需从图表中删除 EA 再重新打开它。
由于点差不断变化,我们必须时常更新品种列表。 此外,某些品种的持仓也在发生变化。 因此,先前启动的 EA 再次处理之前,请不要忘记更新品种列表(按 R 键)来查看当前数据。
我们来查看 start_symbols 函数。 它还可以作为启动其它函数的包装器:
void start_symbols(){ // 将列表中当前品种的索引设置为零(数组中的第一个品种): panel1val=0; // 准备品种列表: prepare_symbols(); // 从图表中删除以前创建的品种按钮: ObjectsDeleteAll(0, exprefix); // 显示品种列表: show_symbols(); // 更新图表来查看变化: ChartRedraw(0); }
我们遇到了另外两个自定义函数:prepare_symbols 和 show_symbols。 第一个形成符合我们条件的品种数组。 第二个在 EA 运行的图表上显示这些品种的按钮。
在图表上显示按钮很简单。 首先,我们找到用于显示按钮的 X 和 Y 坐标,令其不与其它按钮重叠。 然后我们就应显示它:
void show_symbols(){ // 初始化定义 X 和 Y 坐标的变量 int btn_left=0; int btn_line=1; int btn_right=(int) ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-77; // 在图表中显示数组中每个品种的按钮 // 在按钮上书写品种名称 for( int i=0; i<arrPanel1.Total(); i++ ){ if( btn_left>btn_right-BTN_WIDTH ){ btn_line++; btn_left=0; } ObjectCreate(0, exprefix+"btn"+(string) i, OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_XDISTANCE,btn_left); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_FONTSIZE,8); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_COLOR,clrBlack); ObjectSetString(0,exprefix+"btn"+(string) i,OBJPROP_TEXT,arrPanel1.At(i)); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_SELECTABLE,false); btn_left+=BTN_WIDTH; } }
结果就是符合我们条件的品种将显示在图表上:
现在,我们专注于形成用于选择品种的条件(prepare_symbols 函数)。 首先,我们将所有品种添加到列表中:
void prepare_symbols(){ // 临时存储品种名称的变量 string name; // 存储最后品种报价的变量 MqlTick lastme; // 如果品种数组已包含任何数值,则重置品种数组 arrPanel1.Resize(0); // 形成临时 tmpSymbols 数组 // 它将包含所有可用的品种 CArrayString tmpSymbols; for( int i=0; i<SymbolsTotal(noSYMBmarketWath); i++ ){ tmpSymbols.Add(SymbolName(i, noSYMBmarketWath)); } // 条件在这里检查 // 并且品种将包含在品种列表中 // 如果它符合条件 for( int i=0; i<tmpSymbols.Total(); i++ ){ name=tmpSymbols[i]; // 从品种名称中删除多余的空格, // 因为我们不确定它来自何处 StringTrimLeft(name); StringTrimRight(name); if( !StringLen(name) ){ continue; } // 进一步进行品种的主要过滤 // ... // 如果品种符合我们的所有条件,则将其添加到列表中 arrPanel1.Add(name); } }
首先,将所有品种放入临时数组中。 此时,Hide symbols absent in the Market Watch panel 参数定义的初始过滤已经发生。
不必将品种放入临时数组当中去了。 代之,我们可以将必要的品种放入主列表中。 但在这种情况下,我们需要重新编写代码,例如,我们需要添加一个输入,只在列表中添加需要显示的品种。 换言之,我们应以必要顺序的自定义品种替代经纪商提供的所有品种。
出于同样的原因,首先在循环中将临时数组中所有品种枚举清除。 如果您想要实现上述输入,则您不能省略过滤自定义输入。
现在,我们根据我们已有的输入对获得的品种进行排序。 在 品种主要过滤依据 prepare_symbols 函数(循环中向列表添加品种)的注释字符串进一步执行 之下添加了以下代码模块。
隐藏已有交易持仓的品种:
// 隐藏已有交易持仓的品种 bool isskip=false; if( noSYMBwithPOS ){ // 查看所有持仓清单 int cntMyPos=PositionsTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ // 如果当前品种有持仓,则跳过 if(PositionGetSymbol(ti) == name ){ isskip=true; break; } } if(!isskip){ int cntMyPosO=OrdersTotal(); if(cntMyPosO>0){ for(int ti=cntMyPosO-1; ti>=0; ti--){ ulong orderTicket=OrderGetTicket(ti); if( OrderGetString(ORDER_SYMBOL) == name ){ isskip=true; break; } } } } }
首先,检查品种是否有持仓。 如果没有持仓,则检查是否有限价单。 如果存在持仓或限价单,则跳过该品种。
隐藏有点差品种:
// 如果品种当前价格值的输入有效, // 尝试获取当前值 if(hide_PRICE_HIGH>0 || hide_PRICE_LOW>0 || hide_SPREAD>0 ){ SymbolInfoTick(name, lastme); if( lastme.bid==0 ){ Alert("Failed to get BID value. Some filtration functions may not work."); } } if(hide_SPREAD>0 && lastme.bid>0){ switch(hide_SPREAD){ // 如果当前点差超过价格的 0.05%,则跳过该品种 case spread_b05: if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.05 ){ isskip=true; } break; // 如果当前点差超过价格的 0.1%,则跳过该品种 case spread_b1: if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.1 ){ isskip=true; } break; // 如果当前点差超过价格的 0.15%,则跳过该品种 case spread_b15: if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.15 ){ isskip=true; } break; // 如果当前点差低于价格的 0.15%,则跳过该品种 case spread_l15: if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 < 0.15 ){ isskip=true; } break; // 如果当前点差低于价格的 0.1%,则跳过该品种 case spread_l1: if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 < 0.1 ){ isskip=true; } break; } } if(isskip){ continue; }
点差越小越好。 从这个角度来看,最好使用点差小于 0.05% 的品种。 并非所有经纪商都提供这样的良好条件,特别是在股票市场进行交易时。
隐藏价格太高品种 (0 - 不隐藏):
// 隐藏价格太高品种 (0 - 不隐藏) if(hide_PRICE_HIGH>0 && lastme.bid>0 && lastme.bid>hide_PRICE_HIGH){ continue; }
隐藏价格太低品种 (0 - 不隐藏):
if(hide_PRICE_LOW>0 && lastme.bid>0 && lastme.bid<hide_PRICE_LOW){ continue; }
隐藏不可交易品种:
if(hideProhibites){ // 如果品种的最小仓量为 0,则跳过 if( SymbolInfoDouble(name, SYMBOL_VOLUME_MIN)==0 ) continue; // 如果品种禁止开仓,则跳过 if(SymbolInfoInteger(name, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_DISABLED || SymbolInfoInteger(name, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_CLOSEONLY ){ continue; } }
当然,可以在没有任何输入条件的情况下隐藏禁止交易的品种。 但您也许仍希望在列表中包含此类品种。 这就是我们添加此输入的原因。
只显示在指定时段开放的品种:
// 在 curDay 变量中获取当前日期 MqlDateTime curDay; TimeCurrent(curDay); MqlDateTime curDayFrom; datetime dfrom; datetime dto; // 如果市场开放时间有限制 // 并且我们设法获得了当日当前股票的开放时间,然后...... if( hide_HOURS!=hour_any && SymbolInfoSessionTrade(name, (ENUM_DAY_OF_WEEK) curDay.day_of_week, 0, dfrom, dto)){ TimeToStruct(dfrom, curDayFrom); if(hide_HOURS==hour_9am && curDayFrom.hour != 9){ continue; } if(hide_HOURS==hour_10am && curDayFrom.hour != 10){ continue; } if(hide_HOURS==hour_4pm && curDayFrom.hour != 16){ continue; } if(hide_HOURS==hour_0am && curDayFrom.hour != 0){ continue; } }
如果市场关闭,则隐藏。 如果您在周日启动 EA,您大概并非想要分析股票行情。 最有可能的是,您希望选择星期日可用的品种,如 TA25 指数或加密货币。 此输入参数允许我们执行此操作。
当然,它只能显示今天可交易的品种,而非引入单独的输入。 但是,如果是星期天,我们想为下一个交易日做准备,如何选择合适的股票呢? 我们将此功能作为输入参数实现。
若要定义今日市场是否开放,我们需要 SymbolInfoSessionTrade 函数。 如果它返回 false,则显然,这个品种今天无法进行交易。 为了避免调用该函数两次,我们需要重新编写代码,只显示在时段开放的品种:
MqlDateTime curDay; TimeCurrent(curDay); MqlDateTime curDayFrom; datetime dfrom; datetime dto; bool sessionData=SymbolInfoSessionTrade(name, (ENUM_DAY_OF_WEEK) curDay.day_of_week, 0, dfrom, dto); // 如果市场今天关闭,则隐藏一个品种 if( hideClosed && !sessionData ){ continue; } // 只显示在时段开放的品种 // 在 curDay 变量中获取当前日期 // 如果市场开放时间有限定, // 并且我们设法获得了当日当前股票的开放时间,然后...... if( hide_HOURS!=hour_any && sessionData){ TimeToStruct(dfrom, curDayFrom); if(hide_HOURS==hour_9am && curDayFrom.hour != 9){ continue; } if(hide_HOURS==hour_10am && curDayFrom.hour != 10){ continue; } if(hide_HOURS==hour_4pm && curDayFrom.hour != 16){ continue; } if(hide_HOURS==hour_0am && curDayFrom.hour != 0){ continue; } }
如果 ATR 小于设定的美元值则隐藏。 如果您在日内交易并等待价格至少移动 50-90 分钱,那么您不太可能需要每天统计不超过 30 分钱的品种。 此参数允许我们通过指定每日价格走势所需的最小尺寸来筛选品种:
// 如果 ATR 小于设定的美元值则隐藏 if(hideATRcents>0){ MqlRates rates[]; ArraySetAsSeries(rates, true); double atr; if(CopyRates(name, PERIOD_D1, 1, 5, rates)==5){ atr=0; for(int j=0; j<5; j++){ atr+=rates[j].high-rates[j].low; } atr/=5; if( atr>0 && atr<hideATRcents ){ continue; } } }
我认为,通过众多参数足以进行完全过滤。 但是如果您需要其它过滤条件,您可随时在 prepare_symbols 函数的循环中添加它们。
打开图表
我们已经学会了如何绘制符合我们条件的品种按钮。 我们可以在此刻停止并手动打开已获得的品种图表。 但这很不方便。 幸运的是,我们可以简化流程并在单击按钮时打开必要的图表。
若要在单击按钮时执行此操作,应在 MQL 语言标准函数 OnChartEvent 中描述该动作。 该函数拦截任何图表事件。 换言之,图表上发生任何事件时都会调用它。
OnChartEvent 函数具有四个输入。 最开头得参数(id)包含当前被 OnChartEvent 函数拦截的事件 ID。 若要真正了解单击图表按钮后调用 OnChartEvent 函数,请将参数值与必需值进行比较。
按钮点击事件有 CHARTEVENT_OBJECT_CLICK ID。 所以,我们将以下代码添加到 OnChartEvent 函数中,以便处理图表上的按钮点击:
void OnChartEvent(const int id, // 事件 ID const long& lparam, // 长整数型事件参数 const double& dparam, // 双精度型事件参数 const string& sparam) // 字符串型事件参数 { switch(id){ case CHARTEVENT_OBJECT_CLICK: // 单击按钮时执行的代码 break; } }
我们如何准确理解在图表上按下了哪个按钮? OnChartEvent 函数的第二个参数(sparam)可以帮助我们。 对于 CHARTEVENT_OBJECT_CLICK 事件,它包含用户单击的按钮名称。 我们只需将此名称与 EA 生成的名称进行比较即可。 如果这是 EA 按钮,则打开必要的品种图表。 所打开的品种名称取自按钮上写入的文本。 因此,我们将以下代码放在 case CHARTEVENT_OBJECT_CLICK 条件中:
// 如果按钮名称包含由您的 EA 创建的所有图形对象之一, // 则... if( StringFind(sparam, exprefix+"btn")>=0 ){ // 放置当前按钮索引 // (列表中当前品种的位置)至 panel1val 变量 string tmpme=sparam; StringReplace(tmpme, exprefix+"btn", ""); panel1val=(int) tmpme; // 打开当前品种的图表 showcharts(ObjectGetString(0,sparam,OBJPROP_TEXT)); }
使用 showcharts 自定义函数打开所选品种的图表。 该函数不仅可以打开必要的图表,还可以:
- 关闭之前由 EA 打开的图表;
- 如果在 市场观察面板上不存在,则添加该品种;
- 如有必要,将图表切换到烛条模式;
- 更改图表比例(我添加此功能只是因为我使用自定义比例而不是默认比例)。
void showcharts(string name){ // 如果图表已经打开,则关闭它们 closecharts(); // 如果在“市场观察”面板中不存在,则添加该品种 // 如果 "Add chart to Market Watch" 输入为 'true' if( addInfoWatch ){ SymbolSelect(name, true); } // 打开图表并将图表 ID 放在 curChartID 数组中 curChartID[ArrayResize(curChartID,ArraySize(curChartID)+1)-1]=charts[(uchar) ArrayResize(charts,ArraySize(charts)+1)-1].Open( name, PERIOD_D1 ); // 如果 "Open candlestick charts" 输入为 'true', // 将图表切换到烛条模式 if(viewCandle){ ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_MODE, CHART_CANDLES); } // 如果 "Show tick volumes" 输入为 'true', // 显示逐笔报价交易量 if(viewVolumes){ ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_SHOW_VOLUMES, CHART_VOLUME_TICK); } // 将图表比例更改为最方便的 ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_SCALE, 2); // 等待三分之一秒,直到所有更改都得到实施 Sleep(333); // 更新打开的图表以便进行所有更改 ChartRedraw(curChartID[ArraySize(curChartID)-1]); }closecharts 函数关闭以前由 EA 打开的所有图表。 它的代码非常简单:
void closecharts(){ // 如果 EA 已打开图表得数组不为空,那么... if(ArraySize(charts)){ // 持续关闭所有图表 for( int i=0; i<ArraySize(charts); i++ ){ charts[i].Close(); } // 清除图表数组 ArrayFree(charts); } // 如果 EA 已打开图表的 ID 数组不为空,则清除它 if(ArraySize(curChartID)){ ArrayFree(curChartID); } }
显示附加品种信息
它的好处不仅可以打开品种图表,还可以在其上显示辅助数据,例如描述(一些经纪商所用品种名称如此难以理解,以至于它们似乎是某种密码)和最后一天最后小时该品种的走势方向。
这些信息可以使用图形对象显示。 不过,我们将使用更简单的方法。 我们将在图表注释中显示这些信息。
为此,请在调用 Sleep 函数之前将以下代码添加到 showcharts 函数:
// 在图表上显示附加信息 string msg=""; if(showNameSymbol){ StringAdd(msg, getmename_symbol(name)+"\r\n"); } if(showInfoSymbol){ StringAdd(msg, getmeinfo_symbol(name, false)+"\r\n"); } if( StringLen(msg)>0 ){ ChartSetString(curChartID[ArraySize(curChartID)-1], CHART_COMMENT, msg); }
如果 showNameSymbol 输入为 true,调用 getmename_symbol 函数返回品种名称所在行。 如果 showInfoSymbol 输入为 true,调用 getmeinfo_symbol 函数返回品种名称所在行:
string getmename_symbol(string symname){ return SymbolInfoString(symname, SYMBOL_DESCRIPTION); } string getmeinfo_symbol(string symname, bool show=true){ MqlRates rates2[]; ArraySetAsSeries(rates2, true); string msg=""; if(CopyRates(symname, PERIOD_D1, 0, 1, rates2)>0){ if(show){ StringAdd(msg, (string) symname+": "); } StringAdd(msg, "D1 "); if( rates2[0].close > rates2[0].open ){ StringAdd(msg, "+"+DoubleToString(((rates2[0].close-rates2[0].open)/rates2[0].close)*100, 2) +"%"); }else{ if( rates2[0].close < rates2[0].open ){ StringAdd(msg, "-"+DoubleToString(((rates2[0].open-rates2[0].close)/rates2[0].close)*100, 2) +"%"); }else{ StringAdd(msg, "0%"); } } } if(CopyRates(symname, PERIOD_H1, 0, 1, rates2)>0){ StringAdd(msg, ", H1 "); if( rates2[0].close > rates2[0].open ){ StringAdd(msg, "+"+DoubleToString(((rates2[0].close-rates2[0].open)/rates2[0].close)*100, 2)+"% (+"+DoubleToString(rates2[0].close-rates2[0].open, (int) SymbolInfoInteger(symname, SYMBOL_DIGITS))+" "+SymbolInfoString(symname, SYMBOL_CURRENCY_PROFIT)+")"); }else{ if( rates2[0].close < rates2[0].open ){ StringAdd(msg, "-"+DoubleToString(((rates2[0].open-rates2[0].close)/rates2[0].close)*100, 2)+"% (-"+DoubleToString(rates2[0].open-rates2[0].close, (int) SymbolInfoInteger(symname, SYMBOL_DIGITS))+" "+SymbolInfoString(symname, SYMBOL_CURRENCY_PROFIT)+")"); }else{ StringAdd(msg, "0%"); } } } return msg; }
因此,我们将在新打开的图表上看到以下信息:
从键盘管理控 EA
我们仍然在 OnChartEvent 函数中,我们来添加一个按键响应:
- 当按下 R 键时,更新经我们的条件过滤的品种列表:
- 当按下 X 键时,从图表上移除 EA。
事件具有 CHARTEVENT_KEYDOWN ID 可用于处理按键。 所按键的代码在已经提到的 sparam 参数中传递。 所以,我们只需简单地将以下条件添加到 switch 操作符:
case CHARTEVENT_KEYDOWN: switch((int) sparam){ case 45: //x ExpertRemove(); break; case 19: //r start_symbols(); break; } break;
如您所见,当按 R 键时,我们只需调用早前创建的 start_symbols 函数即可。
添加图表导航
我们不仅已学会了如何显示品种按钮,还了解了如何在点击按钮时打开必要品种的图表。 不过,我们在此还未结束。 我们的实用程序使用起来仍然不方便。 打开品种图表后,我们需要手动关闭它然后再点击下一个图表按钮。 这应该在每次我们需要移动到下一个品种时完成,这令操作非常繁琐。 我们来添加品种列表导航按钮来打开图表。
我们仅需添加三个按钮:移动到下一个图表,移动到上一个图表,以及关闭图表。
剩下的只是决定如何实现它们。 我们可以在创建新图表时直接在 showcharts 函数中添加按钮。 但是将来按钮的数量可能会增加。 创建按钮和其它图形对象可能会减慢打开图表的速度,这有悖期望。
因此,我们将在标准 OnTimer 函数中创建按钮。 我们将定期检查 EA 是否打开图表,如果图表已打开,那么它是否有按钮。 如果没有按钮,则创建它们:
void OnTimer() { // 如果已打开图表 ID 数组包含数值,则... uchar tmpCIDcnt=(uchar) ArraySize(curChartID); if(tmpCIDcnt>0 ){ // 如果数组中的最后一个 ID 没有损坏,那么... if(curChartID[tmpCIDcnt-1]>0){ // 如果 ID 对应的图表没有按钮,则创建它们 if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){ createBTNS(curChartID[tmpCIDcnt-1]); } } } }
图表上的按钮是在 createBTNS 自定义函数中创建的。 它的代码非常简单:
void createBTNS(long CID){ ObjectCreate(CID, exprefix+"_p_btn_prev", OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_XDISTANCE,110); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_YDISTANCE,90); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_CORNER,CORNER_LEFT_LOWER); ObjectSetString(CID,exprefix+"_p_btn_prev",OBJPROP_TEXT,"Prev chart"); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_SELECTABLE,false); ObjectCreate(CID, exprefix+"_p_btn_next", OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_XDISTANCE,110); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_YDISTANCE,65); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_CORNER,CORNER_LEFT_LOWER); ObjectSetString(CID,exprefix+"_p_btn_next",OBJPROP_TEXT,"Next chart"); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_SELECTABLE,false); ObjectCreate(CID, exprefix+"_p_btn_close", OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_XDISTANCE,110); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_YDISTANCE,40); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_CORNER,CORNER_LEFT_LOWER); ObjectSetString(CID,exprefix+"_p_btn_close",OBJPROP_TEXT,"Close chart"); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_SELECTABLE,false); // 更新图表以便查看已实施的变化 ChartRedraw(CID); }
结果就是,新图表采用以下形式:
添加按钮响应
到目前为止,添加到图表中的按钮只是一种装饰。 按下它们没有任何反应。 我们指导它们如何响应按压。
不幸地是,标准的 OnChartEvent 函数在此刻没有任何帮助,因为它只针对启动 EA 的图表上发生的事件作出反应,而按钮是被添加到新图表当中。
也许,有一些更方便的方式。 我想到了一种方式来响应另一个图表上发生的变化。 它涉及 OnTimer 标准函数。 如果图表具有按钮,我们将检查它们当中的一些是否被按压。 如果是,则执行必要的动作。 结果就是,条件:
if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){ createBTNS(curChartID[tmpCIDcnt-1]); }
...重写如下:
if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){ createBTNS(curChartID[tmpCIDcnt-1]); }else{ if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_prev",OBJPROP_STATE)==true ){ prevchart(); return; } if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_next",OBJPROP_STATE)==true ){ nextchart(); return; } if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_close",OBJPROP_STATE)==true ){ closecharts(); return; } }
当按压 Prev chart 按钮时,调用 prevchart 函数。 当按压 Next chart 按钮时,调用 nextchart 函数。 当按压 Close chart 按钮时,调用上述 closecharts 函数。 prevchart 和 nextchart 函数均类似:
void nextchart(){ // 如果品种列表具有下一个品种,则打开其图表 // 否则,关闭当前图表 if(arrPanel1.Total()>(panel1val+1)){ panel1val++; showcharts(arrPanel1[panel1val]); }else{ closecharts(); } } void prevchart(){ // 如果品种列表包含上一个符号,则打开其图表 // 否则,关闭当前图表 if(arrPanel1.Total()>(panel1val-1) && (panel1val-1)>=0){ panel1val--; showcharts(arrPanel1[panel1val]); }else{ closecharts(); } }
结束语
就这样。 如您所见,整体代码的数量并不是很大,而优点显而易见。 我们无需反复打开图表再关闭它们。 代之,我们可以点击必要的按钮,然后为我们完成一切。
当然,也许有更多方式可以改进我们的 EA。 但就其目前的形式而言,它已经是一款成熟的产品,可以显著简化股票的选择。
将此实用程序移植到 MQL4
现在,我们来尝试将我们的实用程序移植到 MQL4。 令人惊讶的是,我们只需要重写一个代码模块。 这将花费大约五分钟。
首先,在 MetaEditor 4 中创建一个新的 EA。 之后,将 MQL5 EA 的源代码移至此处。
编译 EA. 当然,尝试以报错结束。 但结果是,我们得到了要修复的错误列表。 只有三处:
- 'PositionsTotal' - 函数未定义
- 'PositionGetSymbol' - 函数未定义
- 'OrderGetTicket' - 函数未定义
双击第一个报错之处,移至相应的 EA 字符串。
'PositionsTotal' - 函数未定义。 在 prepare_symbols 函数代码的以下模块中检测到错误:
int cntMyPos=PositionsTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ // 如果当前品种有持仓,则跳过 if(PositionGetSymbol(ti) == name ){ isskip=true; break; } } if(!isskip){ int cntMyPosO=OrdersTotal(); if(cntMyPosO>0){ for(int ti=cntMyPosO-1; ti>=0; ti--){ ulong orderTicket=OrderGetTicket(ti); if( OrderGetString(ORDER_SYMBOL) == name ){ isskip=true; break; } } } }
MQL4 和 MQL5 语言之间的一个显著差别就是处理仓位和订单。 所以,我们应该按照以下方式重写代码块,以便 EA 在 MetaTrader 4 中可以正常工作:
int cntMyPos=OrdersTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; if(OrderSymbol() == name ){ isskip=true; break; } }
由于 MQL4 仓位和订单之间没有区别,因此生成的代码要小得多。
剩余的错误会自动修复,因为它们发生在我们修复的代码块当中。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/5348