
可视化交易图表(第二部分):数据图形化展示
引言
在这篇文章中,我们将完成在“可视化交易图表(第一部分):选择分析时段”中开始实施的用于在图表上可视化交易的脚本。我们将编写代码来选择用户选择的单个交易的数据,并在图表上绘制必要的数据对象,然后将其作为相应图表的截图保存到文件中。该脚本将使我们能够大大节省与形成交易图表相关的技术工作以及将其作为截图保存以供后续分析的时间。那些不想花时间组装项目的人可以在市场上下载脚本的现成版本。
选择单个交易的数据
与选择某个时间段内的交易数据不同,选择单个交易的数据将极大地简化历史订单选择样例的实现。这里的主要区别在于,我们将使用HistorySelectByPosition()方法来请求历史数据,而不是使用预定义的终端函数HistorySelect()。该方法的参数接收POSITION_IDENTIFIER,我们可以在MetaTrader 5终端中找到它(查看 -> 工具箱 -> 历史 -> Ticket列)。该值将通过全局输入变量inp_d_ticket传递给脚本。
在其他所有方面,Select_one_deal案例的逻辑完全重复了前一个案例逻辑的实现,并在下面的代码中完整呈现,同时为用户插入了相同的信息。
//--- if one deal is needed case Select_one_deal: res = MessageBox("You have selected analysis of one deal. Continue?","",MB_OKCANCEL); // informed in the message if(res == IDCANCEL) // if interrupted by user { printf("%s - %d -> Scrypt was stoped by user.",__FUNCTION__,__LINE__); // informed in the journal return; // interrupted } MessageBox("Please press 'Ok' and wait for the next message until script will be done."); // informed in the message //--- select by one position if(HistorySelectByPosition(inp_d_ticket)) // select position by id { int total = HistoryDealsTotal(); // total deals if(total <= 0) // if nothing found { printf("%s - %d -> Deal was not found.",__FUNCTION__,__LINE__); // notify MessageBox("Deal was not found with this tiket: "+IntegerToString(inp_d_ticket)+". Script is done."); // informed in the message return; } for(int i=0; i<total; i++) // iterate through the number of deals { //--- try to get deals ticket if((ticket=HistoryDealGetTicket(i))>0) // took the deal number { //--- get deals properties position_id = HistoryDealGetInteger(ticket,DEAL_POSITION_ID); // took the main id entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY);// entry or exit? if(entry == DEAL_ENTRY_IN) // if this is an entry { open = HistoryDealGetDouble(ticket,DEAL_PRICE); // take open price time_open =(datetime)HistoryDealGetInteger(ticket,DEAL_TIME); // take open time symbol=HistoryDealGetString(ticket,DEAL_SYMBOL); // take symbol stop_loss = HistoryDealGetDouble(ticket,DEAL_SL); // take Stop Loss take_profit = HistoryDealGetDouble(ticket,DEAL_TP); // take Take Profit //--- magic = (int)HistoryDealGetInteger(ticket,DEAL_MAGIC); // take Magic comment=HistoryDealGetString(ticket,DEAL_COMMENT); // take comment externalID=HistoryDealGetString(ticket,DEAL_EXTERNAL_ID); // take external id volume = HistoryDealGetDouble(ticket,DEAL_VOLUME); // take volume commission = HistoryDealGetDouble(ticket,DEAL_COMMISSION); // take commission value } if(entry == DEAL_ENTRY_OUT) // if this is an exit { close = HistoryDealGetDouble(ticket,DEAL_PRICE); // take close price time_close =(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);// take close time //--- reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(ticket,DEAL_REASON); // take reason swap = HistoryDealGetDouble(ticket,DEAL_SWAP); // take swap profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); // take profit fee = HistoryDealGetDouble(ticket,DEAL_FEE); // take fee } //--- enter data into the main storage //--- check if there is such id if(Find(PositionID,position_id)==-1) // if there is no such deal, { //--- change the dimensions of the arrays ArrayResize(arr_time_open,ArraySize(arr_time_open)+1); // open time ArrayResize(arr_time_close,ArraySize(arr_time_close)+1); // close time ArrayResize(arr_symbol,ArraySize(arr_symbol)+1); // symbols ArrayResize(arr_stop_loss,ArraySize(arr_stop_loss)+1); // stop levels ArrayResize(arr_take_profit,ArraySize(arr_take_profit)+1);// profits ArrayResize(arr_open,ArraySize(arr_open)+1); // entries ArrayResize(arr_close,ArraySize(arr_close)+1); // exits ArrayResize(PositionID,ArraySize(PositionID)+1); // position id //--- ArrayResize(arr_magic,ArraySize(arr_magic)+1); // Magic ArrayResize(arr_extermalID,ArraySize(arr_extermalID)+1); // external id ArrayResize(arr_comment,ArraySize(arr_comment)+1); // comment ArrayResize(arr_volume,ArraySize(arr_volume)+1); // volume ArrayResize(arr_commission,ArraySize(arr_commission)+1); // commission ArrayResize(arr_reason,ArraySize(arr_reason)+1); // reason ArrayResize(arr_swap,ArraySize(arr_swap)+1); // swap ArrayResize(arr_profit,ArraySize(arr_profit)+1); // profit ArrayResize(arr_fee,ArraySize(arr_fee)+1); // fee PositionID[ArraySize(arr_time_open)-1]=position_id; // id if(entry == DEAL_ENTRY_IN) // if this is an entry { arr_time_open[ ArraySize(arr_time_open)-1] = time_open; // deal time arr_symbol[ ArraySize(arr_symbol)-1] = symbol; // instrument symbol arr_stop_loss[ ArraySize(arr_stop_loss)-1] = stop_loss; // deal stop loss arr_take_profit[ ArraySize(arr_take_profit)-1] = take_profit; // deal take profit arr_open[ ArraySize(arr_open)-1] = open; // open price //--- arr_magic[ ArraySize(arr_magic)-1] = magic; // Magic arr_comment[ ArraySize(arr_comment)-1] = comment; // comment arr_extermalID[ ArraySize(arr_extermalID)-1] = externalID; // external id arr_volume[ ArraySize(arr_volume)-1] = volume; // volume arr_commission[ ArraySize(arr_commission)-1] = commission; // commission } if(entry == DEAL_ENTRY_OUT) // if this is an exit { arr_time_close[ ArraySize(arr_time_close)-1] = time_close; // close time arr_close[ ArraySize(arr_close)-1] = close; // close prices //--- arr_reason[ ArraySize(arr_reason)-1] = reason; // reason arr_swap[ ArraySize(arr_swap)-1] = swap; // swap arr_profit[ ArraySize(arr_profit)-1] = profit; // profit arr_fee[ ArraySize(arr_fee)-1] = fee; // fee } } else { int index = Find(PositionID,position_id); // if there was a record already, if(entry == DEAL_ENTRY_IN) // if this was an entry { arr_time_open[index] = time_open; // deal time arr_symbol[index] = symbol; // symbol arr_stop_loss[index] = stop_loss; // deal stop loss arr_take_profit[index] = take_profit; // deal take profit arr_open[index] = open; // open price //--- arr_magic[index] = magic; // Magic arr_comment[index] = comment; // comment arr_extermalID[index] = externalID; // external id arr_volume[index] = volume; // volume arr_commission[index] = commission; // commission } if(entry == DEAL_ENTRY_OUT) // if this is an exit { arr_time_close[index] = time_close; // deal close time arr_close[index] = close; // deal close price //--- arr_reason[index] = reason; // reason arr_swap[index] = swap; // swap arr_profit[index] = profit; // profit arr_fee[index] = fee; // fee } } } } } else { printf("%s - %d -> Error of selecting history deals: %d",__FUNCTION__,__LINE__,GetLastError()); // informed in the journal printf("%s - %d -> Deal was not found.",__FUNCTION__,__LINE__); // informed in the journal MessageBox("Deal was not found with this tiket: "+IntegerToString(inp_d_ticket)+". Script is done."); // informed in the message return; } break;
现在,我们已经描述了这两个选项,并且在程序执行期间所有存储都已填充了必要的数据,我们可以开始在终端图表上显示这些数据。
显示所需图表
为了在图表上保存交易,我们首先需要在程序级别打开具有所需交易对象的新窗口,进行必要的设计设置,包括将右侧缩进进行个别调整,以便整个交易清晰可见,并调用一个预定义函数,该函数将截图保存到所需文件夹。
首先,让我们声明打开所需图表窗口所需的局部变量。“bars”变量将存储图表右侧的偏移值,“chart_width”和“chart_height”变量将存储用于保存的相应尺寸,当打开新图表时,其句柄将存储在“handle”变量中,以便将来访问该图表。
//--- data collected, moving on to printing int bars = -1; // number of bars in a shift int chart_width = -1; // chart width int chart_height =-1; // chart height long handle =-1; // chart handle
在请求打开新的交易品种窗口之前,我们应该先检查这些交易品种在历史上的有效性。此检查绝对必要,以避免在账户上打开“不存在的交易品种”的错误。如果某个交易品种已被保存在交易历史中,这意味着它曾经存在过,那么我有必要在这里解释一下“不存在的交易品种”可能来自哪里。
首先,这可能与经纪商账户类型有关。如今,大多数经纪商为交易者提供多种账户选项,以便在使用的交易策略方面尽可能使他们的使用既盈利又方便。一些账户对开仓收取佣金,但点差很低,而一些账户类型点差很高,但不收取每笔交易的费用。因此,那些进行中期交易的交易者可能不需要为交易支付佣金,而在中期交易中,点差的大小并不是那么重要。相反,那些进行日内小额波动交易的交易者宁愿支付开仓佣金,也不愿因为点差“突然”扩大而遭受损失。通常,经纪商会将此类条件打包成账户类型,如标准账户、黄金账户、铂金账户、ESN等,并为每个账户配置一种交易品种的名称。例如,在标准账户上的EURUSD货币对,在另一个账户类型上可能看起来像EURUSDb、EURUSDz或EURUSD_i,具体取决于经纪商。
此外,交易对象名称可能会根据某些与外汇交易货币对无关的金融工具的到期日而改变,但我们在这里不会详细讨论这一点,因为本文仍然专门讨论货币对。
需要检查交易品种有效性的另一个条件是,在终端的“市场报价”窗口中纯粹技术性地缺少对必要工具的订阅。即使某个品种名称存在于授权账户上,但如果在终端的上下文菜单(查看 -> 市场报价)中没有选择它,我们将无法打开其图表,并且调用函数将随后出现错误。
我们将通过为每个程序安排一个循环来进行检查的实现,如下所示。
for(int i=0; i<ArraySize(arr_symbol); i++) // iterate through all deal symbols
为了检查保存在我们容器中的交易品种的有效性,我们将使用预定义的终端函数 SymbolSelect()。我们将传递给它的第一个参数是以 string(字符串)格式表示的品种名称。这是我们要检查有效性的交易品种。第二个参数是逻辑值 'true'。将 'true' 作为第二个参数传递意味着,如果给定的交易品种是有效的但在“市场报价”中未选中,则它应该在那里被自动选中。完整的检查逻辑如下所示。
//--- check for symbol availability for(int i=0; i<ArraySize(arr_symbol); i++) // iterate through all deal symbols { if(!SymbolSelect(arr_symbol[i],true)) // check if the symbol is in the book and add if not { printf("%s - %d -> Failed to add a symbol %s to the marketbook. Error: %d", __FUNCTION__,__LINE__,arr_symbol[i],GetLastError()); // informed in the journal MessageBox("Failed to add a symbol to the marketbook: "+arr_symbol[i]+ ". Please select 'show all' in the your market book and try again. Script is done."); // informed in the message return; // if failed, abort } }
因此,如果交易品种的有效性检查未通过,我们将终止程序执行,并向用户发出适当的通知。一旦所有有效性检查都通过,我们就可以直接在终端中打开所需的交易品种图表。
首先,我们提供一个名为deal_close_date的辅助变量,其数据类型为MqlDateTime,这将有助于我们后续方便地将所有保存的图表分类到相应的时间段文件夹中。为了在我们的存储中将明确的datetime数据类型转换为MqlDateTime数据类型,我们将使用如下所示的预定义终端函数TimeToStruct()。
MqlDateTime deal_close_date; // deal closure date in the structure TimeToStruct(arr_time_close[i],deal_close_date); // pass date to the structure
图表将根据main_graph、addition_graph、addition_graph_2和addition_graph_3变量中用户定义的数据进行绘制。如果变量包含PERIOD_CURRENT枚举值,则我们不绘制任何图表。如果向变量中输入了特定值(例如PERIOD_D1),则我们将使用此图表进行绘制。我们将对所有输入的变量以下面的形式进行此检查(以下以主变量为例进行说明):
//--- check the main one if(main_graph != PERIOD_CURRENT) // if the main one selected
绘制每个图表将从打开所需交易品种的新图表开始。将使用预定义的终端函数ChartOpen()来打开交易品种图表,同时从存储中传递所需的品种和时间框架,如下所示。
//--- open the required chart handle = ChartOpen(arr_symbol[i],main_graph); // open the necessary symbol chart
一旦图表打开,我们就对其应用我之前提到的所有标准用户设置。为此,我们将使用预定义的终端函数ChartApplyTemplate(),这将极大地帮助我们,并省去我们自己编写代码的工作。ChartApplyTemplate()函数的参数包括从调用ChartOpen()函数获得的图表句柄,以及用户在dailyHistorytemp格式中为交易时间框架指定的模板名称。调用模板应用函数的代码如下所示。
ChartApplyTemplate(handle,main_template); // apply template
对于那些到目前为止还没有在MetaTrader 5终端中使用过模板的人来说,我们在这岔开稍加说明。如果我们使用一个“丑陋”的模板,那么保存的交易屏幕截图可能会变得“令人厌烦”甚至“无用”。按照以下步骤创建您自己的dailyHistorytemp模板:
- 通过“文件 - 新建图表”打开任意品种的图表。
- 图表打开后,按F8打开属性窗口,例如“PropertiesGBPAUD,Daily”。
- 属性窗口包含几个选项卡:常规、显示和颜色。在每个选项卡上,根据您比较熟悉的设置进行设置——例如,针对日线图,然后点击确定。更多详细信息请参见——图表设置(官方终端帮助)。
- 点击确定后,属性窗口关闭,图表呈现为您所需的样式。
- 现在,在上下文菜单中选择“图表 - 模板 - 保存模板”。出现模板保存窗口。在“文件名”中输入dailyHistorytemp.tpl,然后点击保存。
- 之后,在..MQL5\Profiles\Templates终端文件夹中将会出现dailyHistorytemp.tpl文件,您可以在脚本中使用它。需要注意的是,在脚本中输入模板名称时,不应包含.tpl扩展名。
现在让我们回到我们的代码中。一旦应用了所需的模板,我们需要在代码执行中做一个小的延迟,以便给图表加载所需质量的时间。否则,由于需要将所需的历史价格数据加载到终端中,图表可能无法正确显示。例如,如果您有一段时间没有打开图表,终端需要时间来正确显示它。我们将通过预定义的终端函数Sleep()来设置时间延迟,如下所示。
Sleep(2000); // wait for the chart to load
作为延迟,我们将使用2000毫秒或2秒的值,这个值完全是基于实践经验得出的,以确保图表有足够的时间加载,并且脚本的执行不会因为大量交易而耗费过长的时间。如果您想自定义这个值,可以独立地将这个值输入到脚本设置中,以便根据您的设备性能或网络连接速度来加快或减慢这个过程。实践表明,在大多数情况下,两秒钟就足够了。
现在我们需要禁用图表滚动到最新K线值的功能,因为我们正在分析历史数据,并且不需要新的价格变动一直将图表向右移动。这可以通过使用预定义函数ChartSetInteger()将所需图表的CHART_AUTOSCROLL属性设置为'false'来实现,如下所示。
ChartSetInteger(handle,CHART_AUTOSCROLL,false); // disable auto scroll
既然已经禁用了自动滚动,我们首先需要计算对应时间框架图表上左侧的K线数量,以便将图表向历史方向移动,以显示所关注交易的结束时段。我们可以通过预定义的终端函数iBarShift()来获取这个值,将交易品种、图表时间框架和交易结束时间作为参数传递,因为我们希望从交易开始到结束在打印屏幕上看到整个交易过程。在'exact'参数中,我们传递'false',以防历史数据真的非常深(多)。然而,在我们的这个实现中,这并不是那么关键。带有参数的完整方法调用如下所示。
bars = iBarShift(arr_symbol[i],main_graph,arr_time_close[i],false); // get the shift for the deal time
一旦我们知道了所需的图表偏移量,我们就可以显示正好能够捕捉到历史中我们所需交易的那段时间。我们可以通过向预定义的终端变量ChartNavigate()传递以下参数来将图表按我们需要的距离向所需方向移动,如下所示。
ChartNavigate(handle,CHART_CURRENT_POS,-bars+bars_from_right_main); // shifted the chart with a custom margin
为了移动图表,我们传递了图表句柄、ENUM_CHART_POSITION枚举中的 CHART_CURRENT_POS当前位置值,以及之前在bars变量中获得的交易偏移量,同时还加入了用户输入的偏移量,以便评估平仓后价格可能的走势。
在进行了上述图表变换之后,为了确保一切正常,我们调用ChartRedraw()方法,并开始在图表上绘制额外数据以分析历史交易。
为了绘制自定义信息面板元素以及表示开仓和平仓的线条,还有止损和 止盈水平,我们将使用相应的paintDeal()和paintPanel()自定义函数。我们将基于与终端图表交互的标准行为模式来定义这些函数,其中paintDeal()将绘制开仓和平仓价格的线条,以及 止盈 和 止损 水平,而paintPanel()方法将在屏幕角落包含一个包含完整交易信息的表格。
这些方法的详细定义将在下一节提供。在这里,我们仅指出这些方法将在此代码段中被调用。这样做也是从这样一个角度出发,即您不一定必须使用本文中给出的实现来绘制这两组元素。您可以根据自己的需求重新定义它们,同时保持所需的特性。本文中这些方法的实现是在编写代码时图形美观性和信息含量的最佳比例的一个例子。这里的主要目标是在主代码中保持好调用方法的位置。
//--- draw the deal paintDeal(handle,PositionID[i],arr_stop_loss[i],arr_take_profit[i],arr_open[i],arr_close[i],arr_time_open[i],arr_time_close[i]); //--- draw the information panel paintPanel(handle,PositionID[i],arr_stop_loss[i],arr_take_profit[i],arr_open[i], arr_close[i],arr_time_open[i],arr_time_close[i],arr_magic[i],arr_comment[i], arr_extermalID[i],arr_volume[i],arr_commission[i],arr_reason[i],arr_swap[i], arr_profit[i],arr_fee[i],arr_symbol[i],(int)SymbolInfoInteger(arr_symbol[i],SYMBOL_DIGITS));
在方法绘制了交易线条和信息面板到图表上之后,我们可以继续进行实现保存当前图表上所有内容的屏幕截图的步骤。为了做到这一点,我们首先需要确定屏幕截图的未来宽度和高度尺寸,这可以通过使用重新定义的终端函数ChartSetInteger()从打开的图表中请求这些数据来实现,如下所示。
//--- get data by screen size chart_width = (int) ChartGetInteger(handle,CHART_WIDTH_IN_PIXELS); // look at the chart width chart_height = (int) ChartGetInteger(handle,CHART_HEIGHT_IN_PIXELS); // look at the chart height
我们分别传递了ENUM_CHART_PROPERTY_INTEGER枚举值中的CHART_WIDTH_IN_PIXELS(表示图表宽度,单位为像素)和CHART_HEIGHT_IN_PIXELS(表示图表高度,单位为像素)作为显示图表时对应的参数。
在获取了尺寸数据后,我们需要在标准终端文件夹中创建一个路径,用于保存交易屏幕截图的图表。为了防止EA将所有文件放置在一个文件夹中,为方便用户而对其进行分类,我们通过下面这个字符串中的文件名来自动实现分类。
string name_main_screen = brok_name+"/"+ IntegerToString(account_num)+"/"+ IntegerToString(deal_close_date.year)+"-"+IntegerToString(deal_close_date.mon)+ "-"+IntegerToString(deal_close_date.day)+"/"+ IntegerToString(PositionID[i])+"/"+ EnumToString(main_graph)+IntegerToString(PositionID[i])+".png"; // assign the name
将文件分类整理到标准目录中的文件夹中,如图1所示。
图例 1. 按交易分类保存的屏幕截图文件夹地址的结构
我们可以看到,图表文件将按照经纪人名称、账号、执行年份、月份和日期进行排序,这样用户就可以轻松找到所需的交易,而无需在一般列表中查找文件名。不同的时间框架将位于与终端中相应仓位号对应的文件夹内。
我们将直接通过调用预定义的终端函数ChartScreenShot()来保存信息,向该函数传递所需图表的句柄、之前获取的与图表大小对应的截屏尺寸,以及包含整个文件夹地址结构的文件名作为参数,如图1和下面的代码所示。
ChartScreenShot(handle,name_main_screen,chart_width,chart_height,ALIGN_LEFT); // make a screenshot
如果树形结构中指定的文件夹在标准终端文件夹中不存在,终端将自动创建它们,而无需用户干预。
保存文件后,我们可以关闭图表,以免终端视图杂乱无章,特别是如果下载的内容包含账户上的大量历史交易时。我们将使用预定义的终端函数ChartClose()来关闭图表,并向其传递所需图表的句柄,以避免关闭任何不必要的内容。函数调用如下所示。
ChartClose(handle); // closed the chart
对于用户在输入中指定的所有时间段,我们将以类似的方式重复此操作。现在,为了完成我们的脚本,我们需要在主程序之外定义paintDeal()和paintPanel()方法。
在图表上绘制数据对象
为了在打印屏幕图表上方便地放置信息,我们只需要重新定义两个方法,这两个方法将确定用户所需的数据将如何精确绘制。
让我们从描述paintDeal()方法开始。其目标是绘制与开盘价和收盘价位置、止损和止盈位置相关的仓位图形。为此,在主代码体之外声明具有以下签名的方法描述:
void paintDeal(long handlE, ulong tickeT, double stop_losS, double take_profiT, double opeN, double closE, datetime timE, datetime time_closE)
该方法参数中指定的值如下:handlE - 我们将在其上绘图的图表的句柄,tickeT - 交易编号,stop_losS - 如果有的话,为止损的价格,take_profiT - 如果有的话,为止盈,open price - 开盘价(opeN)和close price - 收盘价(closE),deal open time - 交易开盘时间(timE)和deal close time - 交易收盘时间(time_closE)。
让我们从对象的名称开始绘制,该名称将对应于一个不应重复的唯一名称。因此,在名称中,我们将实现一个功能,即该对象以“name_sl_”的形式对应于止损。为了使名称唯一,我们还将添加交易的编号,如下所示。
string name_sl = "name_sl_"+IntegerToString(tickeT); // assign the name
现在我们可以使用预定义的终端函数ObjectCreate()来创建图形对象本身,该函数会在图表上根据历史仓位绘制止损水平。传递的参数是图表句柄和从name_sl变量中获取的唯一名称。指定OBJ_ARROW_LEFT_PRICE作为对象类型,这表示来自ENUM_OBJECT枚举的左侧价格标签,以及实际的价格值和标签被放置在图表上的时间,如下所示:
ObjectCreate(handlE,name_sl,OBJ_ARROW_LEFT_PRICE,0,timE,stop_losS); // create the left label object
既然对象已经创建,接下来我们设置其OBJPROP_COLOR和OBJPROP_TIMEFRAMES字段的值。将OBJPROP_COLOR设置为clrRed,因为止损通常显示为红色,而OBJPROP_TIMEFRAMES设置为OBJ_ALL_PERIODS,以便在所有时间框架上显示。不过,在这个实现中,第二个条件(时间框架设置)并不是关键。一般来说,止损绘制模块将如下所示。
//--- draw stop loss string name_sl = "name_sl_"+IntegerToString(tickeT); // assign the name ObjectCreate(handlE,name_sl,OBJ_ARROW_LEFT_PRICE,0,timE,stop_losS); // create the left label object ObjectSetInteger(handlE,name_sl,OBJPROP_COLOR,clrRed); // add color ObjectSetInteger(handlE,name_sl,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS); // set visibility ChartRedraw(handlE); // redraw
在绘制每个模块后,调用ChartRedraw()方法。
绘制止盈模块的过程与绘制止损模块类似,但有以下不同之处。首先,在唯一对象名称中添加“name_tp_”加上交易编号,并通过clrLawnGreen颜色设置颜色为绿色调色板中的颜色,这是对应于获得利润的传统标识。除此之外,逻辑与止损模块相似,并在此完整呈现。
//--- draw take profit string name_tp = "name_tp_"+IntegerToString(tickeT); // assign the name ObjectCreate(handlE,name_tp,OBJ_ARROW_LEFT_PRICE,0,timE,take_profiT); // create the left label object ObjectSetInteger(handlE,name_tp,OBJPROP_COLOR,clrLawnGreen); // add color ObjectSetInteger(handlE,name_tp,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS); // set visibility ChartRedraw(handlE); // redraw
接下来,我们将实现通过左侧价格标签绘制入场价格的功能。与之前模块的不同之处首先在于对象的唯一名称。我们将在名称前添加“name_open_”。另一个不同之处在于线条颜色设置为clrWhiteSmoke,这样它在图表上就不会太显眼,但其他方面都相同。
//--- draw entry price string name_open = "name_open_"+IntegerToString(tickeT); // assign the name ObjectCreate(handlE,name_open,OBJ_ARROW_LEFT_PRICE,0,timE,opeN); // create the left label object ObjectSetInteger(handlE,name_open,OBJPROP_COLOR,clrWhiteSmoke); // add color ObjectSetInteger(handlE,name_open,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS); // set visibility ChartRedraw(handlE); // redraw
连接交易开盘价和收盘价标签的线条将使用相同的颜色显示。但线条的类型将有所不同。在ObjectCreate()方法参数中创建对象时,我们将传递ENUM_OBJECT枚举的OBJ_TREND值作为第三个参数,以创建一条趋势线。为了正确地在图表上定位趋势线,我们需要为两个点的位置指定额外的参数,每个点都将有两个属性:价格和时间。为此,我们将开盘价opeN和收盘价closE以及收盘时间time_closE和开盘时间timE(假设这里的timE是一个变量名,尽管它可能更标准地命名为time_open以避免混淆)传递给后续参数,如下所示。
//--- deal line string name_deal = "name_deal_"+IntegerToString(tickeT); // assign the name ObjectCreate(handlE,name_deal,OBJ_TREND,0,timE,opeN,time_closE,closE); // create the left label object ObjectSetInteger(handlE,name_deal,OBJPROP_COLOR,clrWhiteSmoke); // add color ObjectSetInteger(handlE,name_deal,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS); // set visibility ChartRedraw(handlE); // redraw
为了完整地在图表上显示交易信息,我们还需要绘制交易的收盘价标签。为此,我们将使用右侧价格标签,这样信息在打印屏幕时将以更悦目的方式显示。要绘制右侧标签,ObjectCreate()方法应接收ENUM_OBJECT枚举中的OBJ_ARROW_RIGHT_PRICE值作为第三个参数,这代表右侧价格标签。对于其余的绘制工作,我们只需要价格和时间信息,这些信息将通过对应的time_closE(收盘时间)和closE(收盘价)变量传递,如下所示。
//--- draw exit price string name_close = "name_close"+IntegerToString(tickeT); // assign the name ObjectCreate(handlE,name_close,OBJ_ARROW_RIGHT_PRICE,0,time_closE,closE);// create the left label object ObjectSetInteger(handlE,name_close,OBJPROP_COLOR,clrWhiteSmoke); // add color ObjectSetInteger(handlE,name_close,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS); // set visibility ChartRedraw(handlE); // redraw
我们自定义的用于绘制仓位入场和出场线条的paintDeal()方法的描述已经完成。现在,我们可以继续描述在paintPanel()方法中绘制完整交易信息面板的方法。
描述绘制面板的方法将需要我们具备一个更复杂的方法结构,这些方法负责绘制文本标签,如OBJ_LABEL类型的文本标签,使用ENUM_OBJECT枚举以及OBJ_RECTANGLE_LABEL对象来创建和设计用户图形界面。让我们声明相应的自定义方法,称为LabelCreate()用于创建文本标签,RectLabelCreate()用于创建矩形标签。我们将从辅助方法开始描述,然后转向描述主要的paintPanel()方法,在该方法中我们将使用这些辅助方法。
总的来说,我们的脚本方法结构将如图2所示。
图例 2. 绘制图形对象的自定义方法的结构
要声明LabelCreate()方法,并为其指定以下参数签名,我们可以按照以下格式进行:
bool LabelCreate(const long chart_ID=0, // chart ID const string name="Label", // label name const int sub_window=0, // subwindow number const long x=0, // X coordinate const long y=0, // Y coordinate const ENUM_BASE_CORNER corner=CORNER_LEFT_UPPER, // chart corner for anchoring const string text="Label", // text const string font="Arial", // font const int font_size=10, // font size const color clr=clrRed, // color const double angle=0.0, // text angle const ENUM_ANCHOR_POINT anchor=ANCHOR_LEFT_UPPER, // anchor type const bool back=false, // in the background const bool selection=false, // select to move const bool hidden=true, // hidden in the list of objects const long z_order=0) // priority for clicking with a mouse
chart_ID参数接收图表的句柄,我们需要在该图表上绘制对象。'name'是对象的唯一名称,而sub_window参数的值为0意味着我们希望在主图表窗口中绘制对象。对象的左上角坐标将分别通过X和Y参数传递。我们可以通过向corner参数传递相应的值来改变对象角与图表的绑定,从标准的左上角开始,但我们将保留其默认值ANCHOR_LEFT_UPPER。将要显示的信息的字符串值传递给text参数。显示类型(如字体类型、大小、颜色和角度)将通过相应的font、font_size、clr和angle参数传递。我们还将使用selection和hidden参数使对象在用户对象列表中隐藏且不可通过鼠标选择。z_order参数将负责鼠标点击的优先级顺序。
让我们从重置错误变量开始描述这个方法,这样在未来就可以通过预定义的终端函数ResetLastError()正确地控制创建对象的结果。在调用ObjectCreate()函数时,通过if逻辑运算符来处理创建OBJ_LABEL类型对象的结果,如下所示。如果对象没有创建成功,则在EA日志中通知用户,并通过return语句中断方法的执行,这是通常的做法。
//--- reset the error value ResetLastError(); //--- create a text label if(!ObjectCreate(chart_ID,name,OBJ_LABEL,sub_window,0,0)) { Print(__FUNCTION__, ": failed to create the text label! Error code = ",GetLastError()); return(false); }
如果对象创建成功,那么我们需要通过预定义的终端函数ObjectSetInteger()、ObjectSetString()和ObjectSetDouble()来初始化对象的属性字段,以使其具有所需的外观。使用ObjectSetInteger()函数可以设置相应的坐标值、对象锚点角度、字体大小、对象锚定方法、颜色、显示模式以及与对象对用户可见性相关的属性。使用ObjectSetDouble()函数来设置字体角度值,而ObjectSetString()函数则用于定义传递的文本内容和用于显示的字体类型。下面是该方法的完整实现:
//--- reset the error value ResetLastError(); //--- create a text label if(!ObjectCreate(chart_ID,name,OBJ_LABEL,sub_window,0,0)) { Print(__FUNCTION__, ": failed to create the text label! Error code = ",GetLastError()); return(false); } //--- set label coordinates ObjectSetInteger(chart_ID,name,OBJPROP_XDISTANCE,x); ObjectSetInteger(chart_ID,name,OBJPROP_YDISTANCE,y); //--- set the chart's corner, relative to which point coordinates are defined ObjectSetInteger(chart_ID,name,OBJPROP_CORNER,corner); //--- set the text ObjectSetString(chart_ID,name,OBJPROP_TEXT,text); //--- set the text font ObjectSetString(chart_ID,name,OBJPROP_FONT,font); //--- set font size ObjectSetInteger(chart_ID,name,OBJPROP_FONTSIZE,font_size); //--- set the text angle ObjectSetDouble(chart_ID,name,OBJPROP_ANGLE,angle); //--- set anchor type ObjectSetInteger(chart_ID,name,OBJPROP_ANCHOR,anchor); //--- set the color ObjectSetInteger(chart_ID,name,OBJPROP_COLOR,clr); //--- display in the foreground (false) or background (true) ObjectSetInteger(chart_ID,name,OBJPROP_BACK,back); //--- enable (true) or disable (false) the mode of moving the label by mouse ObjectSetInteger(chart_ID,name,OBJPROP_SELECTABLE,selection); ObjectSetInteger(chart_ID,name,OBJPROP_SELECTED,selection); //--- hide (true) or display (false) graphical object name in the object list ObjectSetInteger(chart_ID,name,OBJPROP_HIDDEN,hidden); //--- set the priority for receiving the event of a mouse click on the chart ObjectSetInteger(chart_ID,name,OBJPROP_ZORDER,z_order); //--- successful execution return(true);
为了声明RectLabelCreate()方法,并为其指定对象创建参数,我们需要基于预期的功能和属性来定义方法的参数:
bool RectLabelCreate(const long chart_ID=0, // chart ID const string name="RectLabel", // label name const int sub_window=0, // subwindow number const int x=19, // X coordinate const int y=19, // Y coordinate const int width=150, // width const int height=20, // height const color back_clr=C'236,233,216', // background color const ENUM_BORDER_TYPE border=BORDER_SUNKEN, // border type const ENUM_BASE_CORNER corner=CORNER_LEFT_UPPER, // chart corner for anchoring const color clr=clrRed, // flat border color (Flat) const ENUM_LINE_STYLE style=STYLE_SOLID, // flat border style const int line_width=1, // flat border width const bool back=true, // 'true' in the background const bool selection=false, // select to move const bool hidden=true, // hidden in the list of objects const long z_order=0) // priority for clicking with a mouse
RectLabelCreate()方法的参数与先前声明的LabelCreate()方法的参数非常相似,但增加了对矩形标签边框的设置,该边框将作为显示前一个方法对象数据的背景。配置对象边框的附加参数包括:'border':由ENUM_BORDER_TYPE枚举定义的边框类型,默认值为BORDER_SUNKEN,'style':由ENUM_LINE_STYLE枚举定义的边框样式,默认值为STYLE_SOLID以及line_width:边框线宽,为一个整数值。
方法体的定义将与前一个方法非常相似,并且同样包含两个主要部分:对象的创建,以及通过相应的预定义终端方法来定义对象的属性,如下所示。
//--- reset the error value ResetLastError(); // reset error //--- create a rectangle label if(ObjectCreate(chart_ID,name,OBJ_RECTANGLE_LABEL,sub_window,0,0)) // create object { //--- set label coordinates ObjectSetInteger(chart_ID,name,OBJPROP_XDISTANCE,x); // assign x coordinate ObjectSetInteger(chart_ID,name,OBJPROP_YDISTANCE,y); // assign y coordinate //--- set label size ObjectSetInteger(chart_ID,name,OBJPROP_XSIZE,width); // width ObjectSetInteger(chart_ID,name,OBJPROP_YSIZE,height); // height //--- set the background color ObjectSetInteger(chart_ID,name,OBJPROP_BGCOLOR,back_clr); // background color //--- set border type ObjectSetInteger(chart_ID,name,OBJPROP_BORDER_TYPE,border); // border type //--- set the chart corner, relative to which point coordinates are defined ObjectSetInteger(chart_ID,name,OBJPROP_CORNER,corner); // anchor corner //--- set flat border color (in Flat mode) ObjectSetInteger(chart_ID,name,OBJPROP_COLOR,clr); // frame //--- set flat border line style ObjectSetInteger(chart_ID,name,OBJPROP_STYLE,style); // style //--- set flat border width ObjectSetInteger(chart_ID,name,OBJPROP_WIDTH,line_width); // width //--- display in the foreground (false) or background (true) ObjectSetInteger(chart_ID,name,OBJPROP_BACK,back); // default is background //--- enable (true) or disable (false) the mode of moving the label by mouse ObjectSetInteger(chart_ID,name,OBJPROP_SELECTABLE,selection); // is it possible to select ObjectSetInteger(chart_ID,name,OBJPROP_SELECTED,selection); // //--- hide (true) or display (false) graphical object name in the object list ObjectSetInteger(chart_ID,name,OBJPROP_HIDDEN,hidden); // is it visible in the list //--- set the priority for receiving the event of a mouse click on the chart ObjectSetInteger(chart_ID,name,OBJPROP_ZORDER,z_order); // no events //--- successful execution } return(true);
既然所有辅助方法都已经描述完毕,接下来让我们定义主要方法paintPanel()的方法体,该方法将负责绘制整个面板。输入参数将包含向用户显示完整交易信息所需的所有字段,如下所示。
void paintPanel(long handlE, ulong tickeT, double stop_losS, double take_profiT, double opeN, double closE, datetime timE, datetime time_closE, int magiC, string commenT, string externalIDD, double volumE, double commissioN, ENUM_DEAL_REASON reasoN, double swaP, double profiT, double feE, string symboL, int digitS )
与前面的方法一样,第一个参数将负责定义图表的句柄,在该图表上将创建与信息面板相关联的所有对象。所有其他参数将重复历史交易对象的字段。
我们将通过定义用于存储面板大小的变量,以及用于定位显示信息名称的列和之前获取值的列的坐标,来开始实现绘制交易数据面板的方法,如下所示。
int height=20, max_height =0, max_width = 0; // column height and max values for indent int x_column[2] = {10, 130}; // columns X coordinates int y_column[17]; // Y coordinates
“height”变量将存储一个静态值20,作为每列的高度,以确保每一行都能均匀绘制。同时,“max_height”和“max_width”值将分别存储每列的最大高度和最大宽度,以确保绘制的均匀性。所需的X轴和Y轴坐标将分别存储在x_column[]和y_column[]数组中。
现在我们需要声明两个数组,它们将分别存储用于显示标题列和值列的行值。我们将通过string类型的数据数组来声明标题列,如下所示的代码所示。
string column_1[17] = { "Symbol", "Position ID", "External ID", "Magic", "Comment", "Reason", "Open", "Close", "Time open", "Time close", "Stop loss", "Take profit", "Volume", "Commission", "Swap", "Profit", "Fee" };
所有数组值都被声明为静态初始化,因为面板不会更改,且数据将始终以相同的顺序显示。这对于习惯查看不同交易的信息来说应该是很方便的。虽然可以实现一种功能,允许从面板中排除不包含值或等于零的数据,但在快速查找信息时,这样做会很不方便。与每次查看列值相比,在已知的显示模式中搜索信息仍然更加熟悉。
以相同的数据顺序,声明第二个数组,该数组将包含上面数组中声明的列的值。数组声明的描述如下:
string column_2[17] = { symboL, IntegerToString(tickeT), externalIDD, IntegerToString(magiC), commenT, EnumToString(reasoN), DoubleToString(opeN,digitS), DoubleToString(closE,digitS), TimeToString(timE), TimeToString(time_closE), DoubleToString(stop_losS,digitS), DoubleToString(take_profiT,digitS), DoubleToString(volumE,2), DoubleToString(commissioN,2), DoubleToString(swaP,2), DoubleToString(profiT,2), DoubleToString(feE,2) };
该数组将在方法级别局部声明,并且其字段将直接从方法参数中初始化,同时使用相应的预定义终端函数。
既然我们已经声明了包含所需数据的容器,接下来就需要计算每个单元格坐标的锚定值,同时考虑在其中每一个中寻找最大值。我们可以用以下代码来实现这一点:
int count_rows = 1; for(int i=0; i<ArraySize(y_column); i++) { y_column[i] = height * count_rows; max_height = y_column[i]; count_rows++; int width_curr = StringLen(column_2[i]); if(width_curr>max_width) { max_width = width_curr; } } max_width = max_width*10; max_width += x_column[1]; max_width += x_column[0];
在这里,我们通过遍历行数来找到每个对象的锚点坐标,每行乘以一个固定的Y轴高度值。我们还检查每个值是否超过了最大宽度值,以获取X坐标。
一旦我们有了所有带有坐标的值,就可以开始使用之前声明的自定义LabelCreate()方法来绘制信息了。我们将根据要显示的行数循环调用该方法,如下所示。
color back_Color = clrWhiteSmoke; color font_Color = clrBlueViolet; for(int i=0; i<ArraySize(column_1); i++) { //--- draw 1 string name_1 = column_1[i]+"_1_"+IntegerToString(tickeT); LabelCreate(handlE,name_1,0,x_column[0],y_column[i],CORNER_LEFT_UPPER,column_1[i],"Arial",10,font_Color,0,ANCHOR_LEFT_UPPER,false); //--- draw 2 string name_2 = column_1[i]+"_2_"+IntegerToString(tickeT); LabelCreate(handlE,name_2,0,x_column[1],y_column[i],CORNER_LEFT_UPPER,column_2[i],"Arial",10,font_Color,0,ANCHOR_LEFT_UPPER,false); }
在方法的最后,我们只需要使用之前声明和描述的自定义方法RectLabelCreate()根据这些值绘制背景,并更新显示的图表,如下所示。
//--- draw the background RectLabelCreate(handlE,"RectLabel",0,1,height,max_width,max_height,back_Color); ChartRedraw(handlE);
这就完成了对所有方法的描述。项目已经准备好进行组装和使用了。
因此,在使用该脚本后,图表文件将如图3所示。
图例 3. 脚本运行后展示交易数据的结果
我们可以看到,所有关于交易的信息都以概括的形式呈现在一个图表上,这使得用户分析和评估所进行的交易操作变得更加方便。该脚本将这些文件整理到相应的文件夹中,这也使用户能够随时在账户历史记录中找到任何交易操作所需的信息。
结论
通过本文,我们已经完成了用于图表上交易自动化可视化的脚本编写。使用此解决方案,您可以通过纠正选择入场点时可能出现的错误来显著改善您的交易,同时还可以通过选择正确的交易品种和预期的价格冲动位置来提高您整个策略的数学期望值。使用此脚本将为您节省大量准备图表文件的时间,您可以将这些时间用于分析和寻找新的交易思路。最重要的是要记住,市场是不断变化的,为了确保稳定运行,您需要不断保持联系并监控变化。此工具可以帮助您实现这一目标。祝您工作顺利。请在评论中留下您的反馈。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/14961


