在 前一篇文章 中,我们开发了用于整理和选择拥有适当入场点的品种的实用程序。 我们还学习了如何通过各种参数对品种进行分类,以及使用专门设计的按钮浏览品种。 不过,在品种选择方面,情况并不乐观。 目前,我们必须将所选金融产品的代码书写在一片纸上,这对地球上的森林物种产生了非常负面的影响。
在本文中,我们将保护树木免遭砍伐,并将学习如何自动保存在图表上创建的图形对象,令您将来不必持续创建它们。
首先,我们简化地将实用程序移植到 MQL4 语言。 在前一篇文章中,我们将一个代码模块替换为另一个代码模块,来让程序按照 MQL4 的方式工作。 现在我们面临更艰巨的任务。 我们可以用单一语言进行开发,例如 MQL5,然后不断找出那些在 MQL4 中不起作用的代码块,然后用正确的代码将之替换,或者我们可以同时开发两套程序版本:MQL5 和 MQL4。
这两种选择都不是最佳的。 我们应该不断地替换在 MQL4 中不起作用的模块(在每个版本中),或者记住我们在实用程序当中所修改的代码部分,以便用另一种语言实现。
所以,我们将采用不同的方法。 MQL5 和 MQL4 两者都支持条件编译指令,允许执行一个代码模块,或依据条件执行另一个代码模块。 在这些指令当中,有一个构造会根据当前的 MQL 语言版本执行。 其主要语法如下:
#ifdef __MQL5__ // 代码块仅在 MQL5 中执行 #else // 代码块仅在 MQL4 中执行 #endif
我们利用它来让我们的 checkSYMBwithPOS 函数在 MQL5 和 MQL4 上均能正常工作,而无需不停地替换它:
bool checkSYMBwithPOS(string name){ // 隐藏已有持仓和订单的品种 bool isskip=false; if( noSYMBwithPOS ){ #ifdef __MQL5__ // 查看所有持仓清单 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; } } } } #else // 查看所有持仓清单 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; } } #endif } return isskip; }
深入本文,我们将使用此结构立即移植在 MQL4 中不起作用的代码模块。
为了节省地球上的森林,我们将创建三个选项卡,仅显示以前所选的品种。 我们以 Long, Short 和 Range 来命名这些选卡。 当然,您不必在那里独独添加上行,下行或横盘的品种。 您可以自行决定使用它们。
结果则是,在启动我们的实用程序的图表上多出另一行,包括四个按钮:所有(All)按钮和前面描述的三个按钮。
所有(All) 按钮默认已按下,这意味着符合我们过滤器的所有品种列表将显示如下:
因此,目标已经设定。 现在我们必须实现它。 要做到这一点,我们将不得不重写我们实用程序的一小部分。
用于存储选项卡内容的数组。 首先,我们添加变量来存储选项卡的内容。 之前,我们只有一个选项卡,其内容存储在 arrPanel1 变量中。 我们将为其他选卡添加类似的变量:
// 相应选项卡中所显示品种的数组:
CArrayString arrPanel1;
CArrayString arrPanel2;
CArrayString arrPanel3;
CArrayString arrPanel4;
此外,我们将创建另一个数组,以便能够在循环中访问选项卡。 该数组将存储指向以前创建的所有四个数组的指针:
// 用于合并所有选项卡的数组 CArrayString *arrPanels[4];
我们在 OnInit() 函数中初始化数组:
arrPanels[0]=&arrPanel1; arrPanels[1]=&arrPanel2; arrPanels[2]=&arrPanel3; arrPanels[3]=&arrPanel4;
选卡标题。由于在循环中执行选卡操控,所以最好将选卡名称存储在那里,并从循环中访问它们。 因此,我们来创建存储选卡名称的数组:
// 选卡名称数组 string panelNames[4]={"All", "LONG", "SHORT", "Range"};
辅助变量。 另一个变化涉及 panel1val 变量。 我们已将其名称更改为 panelval。 这是一个纯粹的装饰修正,但应该注意。
cur_panel 参数还添加了包含当前激活选项卡索引。 变量类型是 uchar。 这意味着它可能需要 0 到 255 之间的值,这已经足够了,因为我们只有 4 个选项卡。
默认时,第一个选项卡(在数组中索引为 0)处于激活状态。 因此,我们在 OnInit() 函数中添加字符串,为变量赋值 0。 最后,OnInit() 函数的最终形式:
int OnInit() { // 当前激活选项卡的索引 cur_panel=0; // 初始化选项卡数组 arrPanels[0]=&arrPanel1; arrPanels[1]=&arrPanel2; arrPanels[2]=&arrPanel3; arrPanels[3]=&arrPanel4; start_symbols(); //--- 创建定时器 EventSetTimer(1); //--- return(INIT_SUCCEEDED); }
其他修改。 没必要列出所有已实现的修改,因为其中许多变化无关紧要。 那么现在让我们继续讨论主要的修改。 对于较小的变更,您可以自行检验它们,即拿新实用程序的源代码与上篇文章附带的源代码进行比较。
主要变化在于有关新选卡显示的修改。 我们决定在循环中操控我们的选项卡。 我们看看是如何做到这一点的。
由于我们有一行选项卡,我们需要以某种方式显示它。 为了实现它,我们创建了一个单独的函数。 其代码如下:
void show_panel_buttons(){ int btn_left=0; // 定义显示选项卡的最大可能 x 轴坐标。 int btn_right=(int) ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-77; string tmpName=""; for( int i=0; i<ArraySize(panelNames); i++ ){ // 如果新按钮的起始坐标超过最大可能值, // 移到新行。 if( btn_left>btn_right-BTN_WIDTH ){ btn_line++; btn_left=0; } // 如果“homework”选项卡包含品种,添加其编号 // 至选卡名称 tmpName=panelNames[i]; if(i>0 && arrPanels[i].Total()>0 ){ tmpName+=" ("+(string) arrPanels[i].Total()+")"; } // 显示选卡按钮 ObjectCreate(0, exprefix+"panels"+(string) i, OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_XDISTANCE,btn_left); ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_FONTSIZE,8); ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_COLOR,clrBlack); ObjectSetString(0,exprefix+"panels"+(string) i,OBJPROP_TEXT,tmpName); ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_SELECTABLE,false); // 如果按钮选项卡当前处于激活状态, // 令其处于按下状态 if( cur_panel == i ){ ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_STATE, true); } btn_left+=BTN_WIDTH; } }
在调用 show_symbols 函数显示品种按钮之前,我们会在 start_symbols 函数中调用该函数。
show_symbols 函数本身也发生了变化,尽管只是略有修改。 现在我们只显示当前激活i选卡上的品种按钮:
// 在图表中为当前激活选卡的数组中每个品种 // 显示一个按钮 // 我们将在按钮上书写一个品种名称 for( int i=0; i<arrPanels[cur_panel].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,arrPanels[cur_panel].At(i)); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_SELECTABLE,false); if( !noSYMBwithPOS || cur_panel>0 ){ if( checkSYMBwithPOS(arrPanels[cur_panel].At(i)) ){ ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_BGCOLOR,clrPeachPuff); } } btn_left+=BTN_WIDTH; }
用于将品种加入选卡的按钮。 现在我们需要为选定的选项卡添加品种。 我们将借助新按钮 Add LONG, Add SHORT 和 Add Range 在打开的图表页面上完成此操作。
如果您还记得,在上一篇文章中,我们实现了点击所需品种按钮的能力。 单击品种之后,会打开一个图表,其中在左下角包含用于导航整个品种列表的按钮区。 我们将按钮添加到此区域中。 根据此品种是否在相应的选项卡中,该按钮将其添加到选项卡或从中删除。
使用 createBTNS 函数显示按钮区。 加入向其添加新按钮的循环:
for( int i=ArraySize(panelNames)-1; i>0; i-- ){ isyes=false; if(arrPanels[i].Total()){ for(int j=0; j<arrPanels[i].Total(); j++){ if( arrPanels[i].At(j)==name ){ isyes=true; break; } } } if( isyes ){ ObjectCreate(CID, exprefix+"_p_btn_panelfrom"+(string) i, OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_XDISTANCE,110); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_YDISTANCE,tmpHeight); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_CORNER,CORNER_LEFT_LOWER); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_SELECTABLE,false); ObjectSetString(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_TEXT,"Del "+panelNames[i]); ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_BGCOLOR,clrPink); }else{ ObjectCreate(CID, exprefix+"_p_btn_panelto"+(string) i, OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_XDISTANCE,110); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_YDISTANCE,tmpHeight); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_CORNER,CORNER_LEFT_LOWER); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_SELECTABLE,false); ObjectSetString(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_TEXT,"Add "+panelNames[i]); ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_BGCOLOR,clrHoneydew); } tmpHeight+=25; }
若要启用按下新按钮,请将按钮状态的检查代码添加到 OnTimer() 函数。 一切都与我们在上一篇文章中所做的那样:
for( uchar i=1; i<ArraySize(panelNames); i++ ){ // 如果从选卡中删除的按钮被按下,首先,删除品种,并再次打开品种图表 if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_STATE)==true ){ delToPanel(i, ChartSymbol(curChartID[tmpCIDcnt-1])); curchart(); return; } // 如果添加到选项卡的按钮被按下,则首先添加品种,然后打开下一个品种图表 if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_panelto"+(string) i,OBJPROP_STATE)==true ){ addToPanel(i, ChartSymbol(curChartID[tmpCIDcnt-1])); nextchart(); return; } }
delToPanel 函数首先从选定的选项卡中删除品种,然后在启动实用程序的图表上更新所有品种按钮,或仅更新标题按钮:
void delToPanel(uchar num, string name){ // 沿整个数组移动并删除第一个元素 // 其名称与我们的品种相似 for(int i=0; i<arrPanels[num].Total(); i++){ if( arrPanels[num].At(i)==name ){ arrPanels[num].Delete(i); break; } } // 如果选卡当前已打开, if(num==cur_panel){ initial_btn_line(); // 从图表中删除以前创建的品种按钮: ObjectsDeleteAll(0, exprefix); // 显示更新后的品种列表: show_panel_buttons(); show_symbols(); }else{ // 如果打开任何其他选项卡,只需更新标题按钮 upd_panel_title(); } }
addToPanel 函数与我们刚才研究的那个相反。 它会在选项卡中添加一个品种。 除此之外,它还检查品种是否存在于其他选项卡上。 如果品种已存在,则从那处删除:
void addToPanel(uchar num, string name){ // 在选卡中添加一个品种 arrPanels[num].Add(name); // 从其他选卡中删除品种 // 如果存在的话 for( int j=1; j<ArraySize(arrPanels); j++ ){ if(j==num) continue; for(int i=0; i<arrPanels[j].Total(); i++){ if( arrPanels[j].At(i)==name ){ if( panelval==i && i>0 ){ panelval--; } arrPanels[j].Delete(i); break; } } } if(num==cur_panel){ initial_btn_line(); // 从图表中删除以前创建的品种按钮: ObjectsDeleteAll(0, exprefix); // 显示品种列表: show_panel_buttons(); show_symbols(); }else{ upd_panel_title(); } }
在实用程序启动之间保存选项卡内容。 如果我们意外关闭了 EA 会发生什么? 那么我们所有的努力都将化为泡影。 难道我们要再次开始添加一切吗? 我们来确保已添加到 homework 选项卡中的品种列表能够保存到文件里,并在之后重新打开时恢复。
出于某种原因,我们使用 CArrayString 类型对象来存储所选品种的列表。 该类对象的众多优点之一是标准方法,可以轻松地将数组的全部内容发送到文件并从文件中恢复数组。 在关闭实用程序之前,我们利用它们将数组的内容保存到文件中。 换言之,我们应该将新 savePanels 函数的调用添加到标准 OnDeinit() 函数中:
void savePanels(){ for( int i=1; i<ArraySize(arrPanels); i++ ){ fh=FileOpen(exprefix+"panel"+(string) (i+1)+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI); if(fh>=0){ arrPanels[i].Save(fh); FileClose(fh); } } }
数组内容将在标准 OnInit() 函数中恢复:
for( int i=1; i<ArraySize(arrPanels); i++ ){ fh=FileOpen(exprefix+"panel"+(string) (i+1)+".bin",FILE_READ|FILE_BIN|FILE_ANSI); if(fh>=0){ arrPanels[i].Load(fh); FileClose(fh); } }
如果您在不同的市场进行交易,则当从一个市场切换到另一个市场时必须不断调谐实用程序。 毕竟,如果您目前打算交易美国股票市场,并的在市场开盘后的第一个半小时内等待突破,那么您就不需要很久之前就已开盘的欧洲或俄罗斯市场的股票。 与此类似,如果您在俄罗斯市场进行交易,则不需要美国股票。
为了不分散注意力,并专注于必要的品种,为不同国家的市场以及外汇市场创建单独的参数集是合理的,并在必要时加载每个设置文件。 这十分简单,只需要几秒钟的时间。 不过,难以理解的是此刻要加载什么设置。
若要查看所应用的参数集,我们将添加 cmt 输入,在其中我们记下有关当前正在操控的市场的解释:
input string cmt=""; // 用于 (eng) 的参数
我们在选项卡的按钮行显示此注释:
若要实现此目的,请在显示所有按钮之后将以下代码模块添加到 show_panel_buttons hanshu1:
// 如果指定,则显示注释: if(StringLen(cmt)>0){ string tmpCMT=cmt; ObjectCreate(0, exprefix+"title", OBJ_EDIT, 0, 0, 0); ObjectSetInteger(0,exprefix+"title",OBJPROP_XDISTANCE,btn_left+11); ObjectSetInteger(0,exprefix+"title",OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(0,exprefix+"title",OBJPROP_XSIZE,133); ObjectSetInteger(0,exprefix+"title",OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(0,exprefix+"title",OBJPROP_FONTSIZE,8); ObjectSetInteger(0,exprefix+"title",OBJPROP_COLOR,clrGold); ObjectSetInteger(0,exprefix+"title",OBJPROP_BGCOLOR,clrNONE); ObjectSetInteger(0,exprefix+"title",OBJPROP_BORDER_COLOR,clrBlack); ObjectSetString(0,exprefix+"title",OBJPROP_TEXT,tmpCMT); ObjectSetInteger(0,exprefix+"title",OBJPROP_SELECTABLE,false); }
除了识别当前参数集之外,cmt 输入还可以帮助我们分离 homework 选项卡中的品种列表。 毕竟,如果我们为了操作美国市场而在 homework 选卡上添加品种,那么我们操作俄罗斯股票时就不需要这个品种了。 设置文件 具有不同参数集,还应提供用于 homework 选卡的单独列表。
若要实现它们,我们必须略微修改将数组保存到文件中的代码,并从文件中还原它们。 我们考虑将修改后的函数保存到文件中作为示例:
void savePanels(){ string tmpCmt=cmt; StringReplace(tmpCmt, " ", "_"); for( int i=1; i<ArraySize(arrPanels); i++ ){ fh=FileOpen(exprefix+"panel"+(string) (i+1)+tmpCmt+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI); if(fh>=0){ arrPanels[i].Save(fh); FileClose(fh); } } }
另一个需要我们解决的障碍是操控图表自动保存和还原我们已在图表上创建的图形对象。 如果我们在图表上设置一个级别,我们希望在关闭图表窗口并重新打开它时再次看到它。 每次打开品种图表时,我们肯定不乐意重新放置数十个级别。
到目前为止,我们编写的代码在 MQL5 和 MQL4 中运行得同样良好。 然而保存和还原图形对象的函数并非这种情况。 在 MQL4 中,图形对象类型及其单独的属性由数字类型的常量描述,而 MQL5 则为此应用枚举(enum 类型)。 这就是为什么将它们保存到文件并还原它们非常困难的原因。 至少我无法在一般意义上处理这项任务。 保存 MQL4 图形对象的功能对我们来说更有帮助。 从理论上讲,它可以保存任何图形对象(我没有用所有对象测试它,因此有可能出现例外)。 MQL5 的功能仅允许它操控水平线,标签和文本字段。 如果您需要保存其他图形对象,则必须自己实现。
MQL4 由于 MQL4 保存图形对象的代码更简单,让我们从这种语言的函数开始。 将图形对象保存到文件的函数:
void savechart(ulong id){ // 保存图形对象,仅在 // "Save created graphical objects" 参数 = true if(saveGraphics){ // 获取品种名称 string tmpName=""; if(cur_panel<ArraySize(arrPanels)){ tmpName=arrPanels[cur_panel][panelval]; } tmpName=clean_symbol_name(tmpName); StringReplace(tmpName, " ", ""); // 清除图形对象数组 saveG.Resize(0); // 将所有用户创建的图形对象及其属性添加到数组中 int obj_total=ObjectsTotal((long) id); string name; string tmpObjLine=""; for(int i=0;i<obj_total;i++){ name = ObjectName((long) id, i); if( StringFind(name, exprefix)<0 && StringFind(name, "fix")<0 && StringFind(name, "take")<0 && StringFind(name, "stop loss")<0 && StringFind(name, "sell")<0 && StringFind(name, "buy")<0 ){ tmpObjLine=name; StringAdd(tmpObjLine, "|int~OBJPROP_TYPE~"+(string)(int) OBJPROP_TYPE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TYPE)); StringAdd(tmpObjLine, "|int~OBJPROP_COLOR~"+(string)(int) OBJPROP_COLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_COLOR)); StringAdd(tmpObjLine, "|int~OBJPROP_STYLE~"+(string)(int) OBJPROP_STYLE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_STYLE)); StringAdd(tmpObjLine, "|int~OBJPROP_WIDTH~"+(string)(int) OBJPROP_WIDTH+"~"+(string) ObjectGetInteger(id, name, OBJPROP_WIDTH)); StringAdd(tmpObjLine, "|int~OBJPROP_TIME~"+(string)(int) OBJPROP_TIME+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TIME)); StringAdd(tmpObjLine, "|int~OBJPROP_TIMEFRAMES~"+(string)(int) OBJPROP_TIMEFRAMES+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TIMEFRAMES)); StringAdd(tmpObjLine, "|int~OBJPROP_ANCHOR~"+(string)(int) OBJPROP_ANCHOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_ANCHOR)); StringAdd(tmpObjLine, "|int~OBJPROP_XDISTANCE~"+(string)(int) OBJPROP_XDISTANCE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XDISTANCE)); StringAdd(tmpObjLine, "|int~OBJPROP_YDISTANCE~"+(string)(int) OBJPROP_YDISTANCE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YDISTANCE)); StringAdd(tmpObjLine, "|int~OBJPROP_STATE~"+(string)(int) OBJPROP_STATE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_STATE)); StringAdd(tmpObjLine, "|int~OBJPROP_XSIZE~"+(string)(int) OBJPROP_XSIZE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XSIZE)); StringAdd(tmpObjLine, "|int~OBJPROP_YSIZE~"+(string)(int) OBJPROP_YSIZE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YSIZE)); StringAdd(tmpObjLine, "|int~OBJPROP_XOFFSET~"+(string)(int) OBJPROP_XOFFSET+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XOFFSET)); StringAdd(tmpObjLine, "|int~OBJPROP_YOFFSET~"+(string)(int) OBJPROP_YOFFSET+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YOFFSET)); StringAdd(tmpObjLine, "|int~OBJPROP_BGCOLOR~"+(string)(int) OBJPROP_BGCOLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_BGCOLOR)); StringAdd(tmpObjLine, "|int~OBJPROP_BORDER_COLOR~"+(string)(int) OBJPROP_BORDER_COLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_BORDER_COLOR)); StringAdd(tmpObjLine, "|double~OBJPROP_PRICE~"+(string)(int) OBJPROP_PRICE+"~"+(string) ObjectGetDouble(id, name, OBJPROP_PRICE)); StringAdd(tmpObjLine, "|string~OBJPROP_TEXT~"+(string)(int) OBJPROP_TEXT+"~"+(string) ObjectGetString(id, name, OBJPROP_TEXT)); saveG.Add(tmpObjLine); } } // 将数组内容保存到文件中 fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI); if(fh>=0){ saveG.Save(fh); FileClose(fh); } } }
如您所见,我们不保留对象的所有属性,而仅保留 OBJPROP_COLOR, OBJPROP_STYLE, OBJPROP_WIDTH, OBJPROP_TIME, OBJPROP_TIMEFRAMES, OBJPROP_ANCHOR, OBJPROP_XDISTANCE, OBJPROP_YDISTANCE, OBJPROP_STATE, OBJPROP_XSIZE, OBJPROP_YSIZE, OBJPROP_XOFFSET, OBJPROP_YOFFSET, OBJPROP_BGCOLOR, OBJPROP_BORDER_COLOR, OBJPROP_PRICE 和 OBJPROP_TEXT。 如果在使用该实用程序时未能正确保存任何已应用的图形对象,则表示并未保存所有已应用的属性。 在这种情况下,只需向此函数添加保存缺失属性,以便能够支持此类图形对象。
现在我们看看从文件加载图形对象,并在图表上显示它们的函数:
void loadchart(ulong id){ // 显示图形对象,仅在 // "Save created graphical objects" 输入 = true if(saveGraphics){ // 获取品种名称 string tmpName=""; if(cur_panel<ArraySize(arrPanels)){ tmpName=arrPanels[cur_panel][panelval]; } tmpName=clean_symbol_name(tmpName); StringReplace(tmpName, " ", ""); string tmpObjLine[]; string tmpObjName=""; string sep1="|"; string sep2="~"; // 清除图形对象数组 saveG.Resize(0); // 将图形对象列表从文件加载到数组 fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_READ|FILE_BIN|FILE_ANSI); if(fh>=0){ saveG.Load(fh); FileClose(fh); } // 在图表上连续显示图形对象 for( int i=0; i<saveG.Total(); i++ ){ StringSplit(saveG.At(i), StringGetCharacter(sep1,0), tmpObjLine); for( int j=0; j<ArraySize(tmpObjLine); j++ ){ if(j>0){ string tmpObjSubLine[]; StringSplit(tmpObjLine[j], StringGetCharacter(sep2,0), tmpObjSubLine); if(ArraySize(tmpObjSubLine)==4){ if(tmpObjSubLine[0]=="int"){ // 对象类型总是在行中排在第一位 // 这样我们初始创建一个对象之后就能形成其属性 if(tmpObjSubLine[1]=="OBJPROP_TYPE"){ ObjectCreate(id, tmpObjName, (int) tmpObjSubLine[3], 0, 0, 0); }else if( (int) tmpObjSubLine[3] >= 0 ){ ObjectSetInteger(id, tmpObjName, (int) tmpObjSubLine[2], (int) tmpObjSubLine[3]); } }else if(tmpObjSubLine[0]=="double"){ if( (double) tmpObjSubLine[3] >= 0 ){ ObjectSetDouble(id, tmpObjName, (int) tmpObjSubLine[2], (double) tmpObjSubLine[3]); } }else if(tmpObjSubLine[0]=="string"){ if( StringLen(tmpObjSubLine[3]) > 0 ){ ObjectSetString(id, tmpObjName, (int) tmpObjSubLine[2], tmpObjSubLine[3]); } } } }else{ tmpObjName=tmpObjLine[j]; } } ObjectSetInteger(id, tmpObjName, OBJPROP_SELECTABLE, true); } } }
MQL5 如前所述,MQL5 中的这些函数效率不高:
void savechart(ulong id){ if(saveGraphics){ string tmpName=""; if(cur_panel<ArraySize(arrPanels)){ tmpName=arrPanels[cur_panel][panelval]; } tmpName=clean_symbol_name(tmpName); StringReplace(tmpName, " ", ""); saveG.Resize(0); int obj_total=ObjectsTotal((long) id); string name; string tmpObjLine=""; for(int i=0;i<obj_total;i++){ name = ObjectName((long) id, i); if( StringFind(name, exprefix)<0 && StringFind(name, "fix")<0 && StringFind(name, "take")<0 && StringFind(name, "stop loss")<0 && StringFind(name, "sell")<0 && StringFind(name, "buy")<0 ){ tmpObjLine=name; // 我们只能使用 OBJ_HLINE,OBJ_TEXT 和 OBJ_LABEL 对象类型, // 所以,我们跳过其他类型的对象 if( ObjectGetInteger(id, name, OBJPROP_TYPE)!=OBJ_HLINE && ObjectGetInteger(id, name, OBJPROP_TYPE)!=OBJ_TEXT && ObjectGetInteger(id, name, OBJPROP_TYPE)!=OBJ_LABEL ){ continue; } StringAdd(tmpObjLine, "|int~OBJPROP_TYPE~"+(string)(int) OBJPROP_TYPE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TYPE)); StringAdd(tmpObjLine, "|int~OBJPROP_COLOR~"+(string)(int) OBJPROP_COLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_COLOR)); StringAdd(tmpObjLine, "|int~OBJPROP_STYLE~"+(string)(int) OBJPROP_STYLE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_STYLE)); StringAdd(tmpObjLine, "|int~OBJPROP_WIDTH~"+(string)(int) OBJPROP_WIDTH+"~"+(string) ObjectGetInteger(id, name, OBJPROP_WIDTH)); StringAdd(tmpObjLine, "|int~OBJPROP_TIME~"+(string)(int) OBJPROP_TIME+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TIME)); StringAdd(tmpObjLine, "|int~OBJPROP_TIMEFRAMES~"+(string)(int) OBJPROP_TIMEFRAMES+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TIMEFRAMES)); StringAdd(tmpObjLine, "|int~OBJPROP_ANCHOR~"+(string)(int) OBJPROP_ANCHOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_ANCHOR)); StringAdd(tmpObjLine, "|int~OBJPROP_XDISTANCE~"+(string)(int) OBJPROP_XDISTANCE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XDISTANCE)); StringAdd(tmpObjLine, "|int~OBJPROP_YDISTANCE~"+(string)(int) OBJPROP_YDISTANCE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YDISTANCE)); StringAdd(tmpObjLine, "|int~OBJPROP_STATE~"+(string)(int) OBJPROP_STATE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_STATE)); StringAdd(tmpObjLine, "|int~OBJPROP_XSIZE~"+(string)(int) OBJPROP_XSIZE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XSIZE)); StringAdd(tmpObjLine, "|int~OBJPROP_YSIZE~"+(string)(int) OBJPROP_YSIZE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YSIZE)); StringAdd(tmpObjLine, "|int~OBJPROP_XOFFSET~"+(string)(int) OBJPROP_XOFFSET+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XOFFSET)); StringAdd(tmpObjLine, "|int~OBJPROP_YOFFSET~"+(string)(int) OBJPROP_YOFFSET+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YOFFSET)); StringAdd(tmpObjLine, "|int~OBJPROP_BGCOLOR~"+(string)(int) OBJPROP_BGCOLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_BGCOLOR)); StringAdd(tmpObjLine, "|int~OBJPROP_BORDER_COLOR~"+(string)(int) OBJPROP_BORDER_COLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_BORDER_COLOR)); StringAdd(tmpObjLine, "|double~OBJPROP_PRICE~"+(string)(int) OBJPROP_PRICE+"~"+(string) ObjectGetDouble(id, name, OBJPROP_PRICE)); StringAdd(tmpObjLine, "|string~OBJPROP_TEXT~"+(string)(int) OBJPROP_TEXT+"~"+(string) ObjectGetString(id, name, OBJPROP_TEXT)); saveG.Add(tmpObjLine); } } fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI); if(fh>=0){ saveG.Save(fh); FileClose(fh); } } } void loadchart(ulong id){ if(saveGraphics){ string tmpName=""; if(cur_panel<ArraySize(arrPanels)){ tmpName=arrPanels[cur_panel][panelval]; } tmpName=clean_symbol_name(tmpName); StringReplace(tmpName, " ", ""); string tmpObjLine[]; string tmpObjName=""; string sep1="|"; string sep2="~"; saveG.Resize(0); fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_READ|FILE_BIN|FILE_ANSI); if(fh>=0){ saveG.Load(fh); FileClose(fh); } for( int i=0; i<saveG.Total(); i++ ){ StringSplit(saveG.At(i), StringGetCharacter(sep1,0), tmpObjLine); for( int j=0; j<ArraySize(tmpObjLine); j++ ){ if(j>0){ string tmpObjSubLine[]; StringSplit(tmpObjLine[j], StringGetCharacter(sep2,0), tmpObjSubLine); if(ArraySize(tmpObjSubLine)==4){ if(tmpObjSubLine[0]=="int"){ // 根据类型创建对象 if(tmpObjSubLine[1]=="OBJPROP_TYPE"){ switch((int) tmpObjSubLine[3]){ case 1: ObjectCreate(id, tmpObjName, OBJ_HLINE, 0, 0, 0); break; case 101: ObjectCreate(id, tmpObjName, OBJ_TEXT, 0, 0, 0); break; case 102: ObjectCreate(id, tmpObjName, OBJ_LABEL, 0, 0, 0); break; } }else if( (int) tmpObjSubLine[3] >= 0 ){ if(tmpObjSubLine[1]=="OBJPROP_COLOR"){ ObjectSetInteger(id, tmpObjName, OBJPROP_COLOR, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_STYLE"){ ObjectSetInteger(id, tmpObjName, OBJPROP_STYLE, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_WIDTH"){ ObjectSetInteger(id, tmpObjName, OBJPROP_WIDTH, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_TIME"){ ObjectSetInteger(id, tmpObjName, OBJPROP_TIME, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_TIMEFRAMES"){ ObjectSetInteger(id, tmpObjName, OBJPROP_TIMEFRAMES, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_ANCHOR"){ ObjectSetInteger(id, tmpObjName, OBJPROP_ANCHOR, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_XDISTANCE"){ ObjectSetInteger(id, tmpObjName, OBJPROP_XDISTANCE, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_YDISTANCE"){ ObjectSetInteger(id, tmpObjName, OBJPROP_YDISTANCE, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_STATE"){ ObjectSetInteger(id, tmpObjName, OBJPROP_STATE, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_XSIZE"){ ObjectSetInteger(id, tmpObjName, OBJPROP_XSIZE, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_YSIZE"){ ObjectSetInteger(id, tmpObjName, OBJPROP_YSIZE, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_XOFFSET"){ ObjectSetInteger(id, tmpObjName, OBJPROP_XOFFSET, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_YOFFSET"){ ObjectSetInteger(id, tmpObjName, OBJPROP_YOFFSET, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_BGCOLOR"){ ObjectSetInteger(id, tmpObjName, OBJPROP_BGCOLOR, (int) tmpObjSubLine[3]); }else if(tmpObjSubLine[1]=="OBJPROP_BORDER_COLOR"){ ObjectSetInteger(id, tmpObjName, OBJPROP_BORDER_COLOR, (int) tmpObjSubLine[3]); } } }else if(tmpObjSubLine[0]=="double"){ if( (double) tmpObjSubLine[3] >= 0 ){ if(tmpObjSubLine[1]=="OBJPROP_PRICE"){ ObjectSetDouble(id, tmpObjName, OBJPROP_PRICE, (double) tmpObjSubLine[3]); } } }else if(tmpObjSubLine[0]=="string"){ if( StringLen(tmpObjSubLine[3]) > 0 ){ if(tmpObjSubLine[1]=="OBJPROP_TEXT"){ ObjectSetString(id, tmpObjName, OBJPROP_TEXT, tmpObjSubLine[3]); } } } } }else{ tmpObjName=tmpObjLine[j]; } } ObjectSetInteger(id, tmpObjName, OBJPROP_SELECTABLE, true); } } }
如果我们仔细查看一下,我们会注意到我们必须使用单独的字符串为不同类型的对象创建对象,而在 MQL4 中,一个字符串足以应付所有对象。 对象属性也是如此。 在 MQL4 中,我们使用了一个属性创建字符串(字符串,实数或整数)。 在 MQL5 中,每个属性都需要一个单独的创建字符串。
合并语言 我们来使用条件编译,以便 EA 根据语言应用必要的函数版本:
#ifdef __MQL5__ void savechart(ulong id){ // MQL5 函数 } void loadchart(ulong id){ // ... } #else void savechart(ulong id){ // MQL4 函数 } void loadchart(ulong id){ // ... } #endif
应用函数 现在,我们在程序的相应部分添加调用函数。
loadchart 函数的调用添加在 showcharts 函数中,该函数根据我们按下的按钮打开图表。
图表保存函数的调用将添加到图表导航按钮相应的按下响应代码模块:Next chart, Prev chart 和 Close chart,以及用于在 homework 选卡中添加/删除品种的按钮。
在前一篇文章中,我们提到过滤的品种列表不仅可以从经纪商提供的品种列表中获取,还可以从输入中获取。 首先,这允许以必要的顺序显示仅为有限的品种集合。 其次,自定义品种集合可用在 finviz.com 或类似网站上执行初步过滤。
在前一篇文章中,我们形成了一组输入,允许用户按价格,ATR 等对品种进行排序。 然而,与 finviz.com 网站筛选器相比,这些功能显得苍白无力。 最重要的是,MQL4 无法按照实际交易量对品种进行排序,而在许多基于级别交易的策略中,这是一个非常重要的指标。 finviz.com 网站允许您按照股票的平均交易量,以及当日交易量进行排序。
添加从输入参数获取品种列表的能力。 若要使用第三方品种列表,我们为该实用程序添加三个额外的输入:
input string ""; // 仅品种 (分隔符 - ; 或空格) input string ""; // 为品种加入前缀 input string ""; // 为品种加入后缀
如果经纪商提供的品种名称与官方代码不同,我们需要 onlySymbolsPrefix 和 onlySymbolsSuffix 参数。 一些经纪商为美国股票添加 .us 后缀,为欧洲股票添加 .eu 后缀,而有些经纪商为所有代码添加 m 后缀。 经纪商也可以在股票代码开头添加 #。
添加从文件导入品种的能力。 事到临头,我将立即谈及我们从输入中导入品种时遇到问题。 此问题涉及最大字符串长度。 在使用输入时,我们受限于最多 15-20 个代码。 因此,输入仅可用于少量品种限定的金融产品。
因此,除了输入之外,您还可将必要的品种放在 Files 文件夹中创建的 symbols.txt 文件中。
在代码中实现。 我们将 All 选项卡中品种列表的形成过程划分为两个模块。
第一个模块检查文件或输入中是否有品种。 如果有,则用它们填充 result 数组。 该数组被添加到 OnInit() 函数中:
// 如果 "Symbols only (separator - ; or space)" 输入 // 提供任何数据 if( StringLen(onlySymbols)>0 ){ // 将输入行切分为数组元素 // 元素由 ";" 分隔 StringSplit(onlySymbols,StringGetCharacter(";",0),result); if( ArraySize(result)>1 ){ }else{ // 如果拆分结果在数组中只存在一个数值, // 则拆分失败,显然行中的分隔符是空格 // 所以,现在我们将输入行切分为数组元素 // 且用空格作为元素分隔符 StringSplit(onlySymbols,StringGetCharacter(" ",0),result); } // 否则,检查 Files 文件夹是否包含 symbols.txt 文件 }else if( FileIsExist("symbols.txt") ){ // 如果文件存在,则将其内容放入 'outfile' 临时变量中 int filehandle=FileOpen("symbols.txt",FILE_READ|FILE_TXT); if(filehandle>=0){ string outfile=FileReadString(filehandle); // 如果 'outfile' 变量包含一个字符串, // 尝试将其拆分为数组元素 // 首先,使用分隔符 ";" 接着是空格 if(StringLen(outfile)>0){ StringSplit(outfile,StringGetCharacter(";",0),result); if( ArraySize(result)>1 ){ }else{ StringSplit(outfile,StringGetCharacter(" ",0),result); } if( ArraySize(result)>1 ){ from_txt=true; } } FileClose(filehandle); } }
在 prepare_symbols 函数中,我们首先检查 result 数组是否包含任何数据,如果有,则使用它们。 否则,我们使用经纪商提供的所有品种,或以加到 市场观察 面板的品种继续进行排序:
// 如果数组拥有两个以上的品种,则使用它们 // 如有必要,初步添加所需的后缀或前缀 if( ArraySize(result)>1 ){ for(int j=0;j<ArraySize(result);j++){ StringReplace(result[j], " ", ""); if(StringLen(result[j])<1){ continue; } tmpSymbols.Add(onlySymbolsPrefix+result[j]+onlySymbolsSuffix); } // 否则,使用经纪商提供的所有品种 }else{ for( int i=0; i<SymbolsTotal(noSYMBmarketWath); i++ ){ tmpSymbols.Add(SymbolName(i, noSYMBmarketWath)); } }
利用 finviz.com 筛选器形成品种列表。 最后,我们来看看如何将 finviz.com 上选择的代码导入我们的实用程序。
一切都很简单。 排序之后,转到筛选页面上的 Tickers 选项卡。 您将看到由选定的代码名称组成的云。 全部选中它们,复制并粘贴到 symbols.txt 文件或输入当中。 如果有多个包含排序结果的页面,请转到下一页并执行相同操作。
我们已完成了很多工作,令我们的实用程序更具功能性。 现在我们可以轻松地使用它,忘掉纸质记事本。 我希望,这个星球上的森林会感激这一点。 =)
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程