
自定义指标:为净额结算账户绘制部分入场、出场和反转交易
内容
1. 引言
当我们谈论指标时,我们可以想到不同的功能:绘制(柱状图、趋势线、箭头或条形图)、基于价格和成交量变动计算数据,以及观察交易中的统计模式。然而,在本文中,我们将考虑在MQL5中构建指标的另一种方式。我们将讨论如何管理自己的仓位,包括入场、部分出场等我们将广泛使用动态矩阵以及一些与交易历史和未平仓头寸相关的交易函数。
2. 什么是净额账户
正如文章标题所暗示的,这个指标只适用于净额结算系统账户才有意义。在这种系统中,只允许持有一个相同交易品种的头寸。如果我们朝一个方向交易,头寸规模将增加。如果交易是朝相反方向进行的,那么未平仓头寸将有三种可能的情况:
- 新的交易量较小 -> 头寸减少
- 交易量相等 -> 头寸关闭
- 新的交易量较大 -> 头寸反转
例如,在一个对冲账户上,我们可以进行两次1手的EURUSD买入交易,这将导致同一工具出现两个不同的头寸。在一个净额账户上进行两次1手的EURUSD买入交易,将导致创建一个2手的单一头寸,其价格为两次交易的加权平均价。由于两次交易的交易量相等,头寸价格是每次交易价格的算术平均值。
这种计算是这样进行的:
我们有一个加权平均价格(P),它根据每笔交易的交易量(N)以手数为单位进行加权。
关于这两种系统之间差异的更详细信息,我建议阅读MetaQuotes撰写的文章:《MetaTrader 5的对冲仓位核算系统》。从这一点开始,本文将探讨在净额结算账户上执行的所有操作。如果您还没有这样的账户,可以按照以下步骤在MetaQuotes开设一个免费的模拟账户。
在MetaTrader 5中,点击“文件”>“开设账户”:
开设模拟账户后,点击“继续”按钮,并保持“交易中使用对冲”选项未勾选。
3. 处理交易事件
处理交易事件是管理订单、交易和仓位所必需的。交易订单可以是即时的或待处理的,一旦订单执行,就会生成交易,这些交易可以打开、关闭或修改仓位。
指标不允许使用如OrderSend()之类的函数,但它们可以与交易历史和仓位属性进行交互。通过使用OnCalculate函数,指标可以获取诸如开盘价、收盘价、成交量等信息。尽管OnTrade()函数主要在EA中使用,但它也适用于指标,因为它们可以在策略测试器之外检测交易事件,从而更快、更高效地更新图表对象。
4. 用法实例引言
下图展示了我们正在开发的自定义指标的实际使用示例。这是一个3手的买入仓位,来自一笔较大的交易,该交易经历了多次部分入场和出场(甚至反转)。这也解释了图表上显示的平均价格与报价价格之间以及手数之间的明显差异。通过监控交易事件,您可以了解算法删除线条和在部分出场发生时更新屏幕上手数的依据。
5. 指标属性
在描述的开头,需要指定指标的属性。我们的指标具有以下属性:
#property indicator_chart_window // Indicator displayed in the main window #property indicator_buffers 0 // Zero buffers #property indicator_plots 0 // No plotting //--- plot Label1 #property indicator_label1 "Line properties" #property indicator_type1 DRAW_LINE // Line type for the first plot #property indicator_color1 clrRoyalBlue // Line color for the first plot #property indicator_style1 STYLE_SOLID // Line style for the first plot #property indicator_width1 1 // Line width for the first plot
这段代码影响指标加载页上显示的信息,该页面上设置初始参数。
6. 算法描述
程序开始时执行的OnInit()函数负责初始化一个名为“Element”的double类型数组,该数组作为实际的指标缓冲区。这个数组由三列组成,每个索引分别存储价格(0)、成交量(1)和单号(2)。该数组的每一行对应于历史记录中的某一笔交易。如果初始化成功,即确认该账户不是对冲账户,则会触发OnTrade()函数。如果初始化过程中发生错误,指标将被关闭并从图表中移除。
参阅:
int OnInit() { ArrayResize(Element,0,0); int res=INIT_SUCCEEDED; if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) res=INIT_FAILED; else OnTrade(); return(res); }
初始化完成后,OnTrade() 函数会在交易事件发生时由 OnCalculate() 函数触发。为了确保它仅在形成新的K线时触发一次,我们通过 isNewBar 函数和布尔变量 isOldBar 添加了一个过滤器。因此,OnTrade 函数在三种情况下被激活:初始化时、出现新K线时以及每个交易事件发生时。这些过程提供了对事件的读取、处理和存储,这些事件被存储在 Element 数组中,然后以线条和文本的形式作为图形对象显示在屏幕上。
OnTrade() 函数更新交易算法的关键变量。它从一个名为“date”的 datetime 类型变量开始,该变量存储从订单历史中选择的起始时间。如果程序开始时没有未平仓头寸,“date”变量将用当前K线的开盘时间更新。
当交易被执行时,PositionsTotal() 函数返回一个大于零的值,并通过一个循环过滤出与指标运行的图表对应的交易品种的头寸。然后选择历史记录并检索与头寸ID对应的已执行订单。“date”变量用这些订单中最古老的时间更新,这对应于ID创建的时间。
如果出现具有不同ID的第二个头寸,您需要检查是否有图形元素需要通过 ClearRectangles() 函数删除,以确保一切保持最新状态。之后,我们将 Element 数组的大小设置为零,这将移除它包含的数据。如果没有未平仓头寸,该函数还会激活 ClearRectangles() 函数并重置 Element 数组。“date”变量存储最后已知的服务器时间的值,即当前时间。最后,将“date”变量的剩余值传递给 ListOrdersPositions() 函数。
void OnTrade() { //--- static datetime date=0; if(date==0) date=lastTime; long positionId=-1,numberOfPositions=0; for(int i=PositionsTotal()-1; i>=0; i--) if(m_position.SelectByIndex(i)) if(m_position.Symbol()==ativo000) { numberOfPositions++; positionId=m_position.Identifier(); oldPositionId=positionId; } if(numberOfPositions!=0) { //Print("PositionId: "+positionId); HistorySelectByPosition(positionId); date=TimeCurrent(); for(int j=0; j<HistoryDealsTotal(); j++) { ulong ticket = HistoryDealGetTicket(j); if(ticket > 0) if(HistoryDealGetInteger(ticket,DEAL_TIME)<date) date=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME); } if(HistoryDealsTotal()==1 && (ArraySize(Element)/3)>1) if(ClearRectangles()) ArrayResize(Element,0,0); } else { bool isClean=ClearRectangles(); ArrayResize(Element,0,0); if(isClean) date=TimeCurrent(); // Do not use the array until there is new open position ArrayPrint(Element); // If there are no errors, this function will not be called here: the array with zero size } ListOrdersPositions(date); }
ListOrdersPositions() 函数发挥着重要作用,因为它负责激活向 Element 数组中添加或移除条目的函数:AddValue() 和 RemoveValue() 函数。当接收到 int 类型的参数 dateStart 时,会有两种可能的情况。如果在 HistorySelect(start, end) 函数指定的期间内没有交易历史,它将直接跳转到历史的末尾,并调用 PlotRectangles() 函数,该函数根据 Element 数组的内容更新屏幕上的对象。但如果历史中有交易,HistoryDealsTotal() 函数应返回非零值。在这种情况下,会进行新的检查以研究每个找到的交易,按入场类型对其进行分类,并收集有关价格、成交量和单号的信息。可能的交易类型包括:DEAL_ENTRY_IN、DEAL_ENTRY_OUT 或 DEAL_ENTRY_INOUT。
如果交易是入场交易,则激活 AddValue() 函数。如果是出场交易,则用之前收集到的价格、成交量和单号作为参数激活 RemoveValue() 函数。如果是反转交易,那么如果单号尚未输入数组,还会触发 AddVolume() 函数。此外,还会传递价格和成交量参数,其中成交量计算为收集到的成交量与数组中仍存在的先前交易的成交量之间的差值。
这个过程模拟了历史仓位的重建:当我们遇到一个反转交易时,仓位被反转,并作为一个新的入场交易被加入数组,以调整手数。此外,屏幕上到目前为止的线条将被删除。Sort() 函数按价格列对 Element 数组进行升序排序,并移除数组中第1列(成交量)值为零的图表对象。最后,该函数检查不一致性,并移除数组中索引为0和1(价格和成交量)等于零的行。
void ListOrdersPositions(datetime dateInicio) { //Analyze the history datetime inicio=dateInicio,fim=TimeCurrent(); if(inicio==0) return; HistorySelect(inicio, fim); double deal_price=0, volume=0,newVolume; bool encontrouTicket; uint tamanhoElement=0; for(int j=0; j<HistoryDealsTotal(); j++) { ulong ticket = HistoryDealGetTicket(j); if(ticket <= 0) return; if(HistoryDealGetString(ticket, DEAL_SYMBOL)==_Symbol) { encontrouTicket=false; newVolume=0; // Need to reset each 'for' loop volume=HistoryDealGetDouble(ticket,DEAL_VOLUME); deal_price=HistoryDealGetDouble(ticket,DEAL_PRICE); double auxArray[1][3] = {deal_price,volume,(double)ticket}; if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_IN) AddValue(deal_price,volume,(double)ticket); if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_OUT) RemoveValue(deal_price,volume,(double)ticket); if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT) { tamanhoElement = ArraySize(Element)/3; //Always check the array size, it can vary with the Add/RemoveValue() functions for(uint i=0; i<tamanhoElement; i++) if(Element[i][2]==ticket) { encontrouTicket=true; break; } if(!encontrouTicket) // If after the previous scanning we don't find mentioning of the ticket in the array { for(uint i=0; i<tamanhoElement; i++) { newVolume+=Element[i][1]; Element[i][1]=0; } newVolume=volume-newVolume; AddValue(deal_price,newVolume,double(ticket)); } } } } PlotRectangles(); }
7. 另一个实例
上述对算法的描述足以让我们更清晰地理解其运行机制。现在,让我们通过一个示例更详细地探讨它,该示例展示了涉及的操作以及最重要变量的内容。这些交易是在策略测试器之外执行的,并且交易事件将被检测到。我们知道,在净额结算账户中,每个仓位的交易都有相同的标识符,因此我们可以根据这一标准对它们进行筛选。以下是一个特定仓位的交易事件示例:
时间 | 品种 | 交易 | 类型 | 方向方向 | 交易量 | 价格 |
---|---|---|---|---|---|---|
2023.05.04 09:42:05 | winm23 | 1352975 | buy | in | 1 | 104035 |
2023.05.04 09:43:16 | winm23 | 1356370 | sell | in/out | 2 | 103900 |
2023.05.04 16:34:51 | winm23 | 2193299 | buy | out | 1 | 103700 |
2023.05.04 16:35:05 | winm23 | 2193395 | buy | in | 1 | 103690 |
2023.05.04 16:35:24 | winm23 | 2193543 | buy | in | 1 | 103720 |
2023.05.04 16:55:00 | winm23 | 2206914 | sell | out | 1 | 103470 |
2023.05.04 17:27:26 | winm23 | 2214188 | sell | in/out | 2 | 103620 |
2023.05.04 17:30:21 | winm23 | 2215738 | buy | in/out | 4 | 103675 |
2023.05.05 09:03:28 | winm23 | 2229482 | buy | in | 1 | 104175 |
2023.05.05 09:12:27 | winm23 | 2236503 | sell | out | 1 | 104005 |
2023.05.05 09:19:18 | winm23 | 2246014 | sell | out | 1 | 103970 |
2023.05.05 09:22:45 | winm23 | 2250253 | buy | in | 1 | 103950 |
2023.05.05 16:00:10 | winm23 | 2854029 | sell | out | 1 | 106375 |
2023.05.05 16:15:40 | winm23 | 2864767 | sell | out | 1 | 106275 |
2023.05.05 16:59:41 | winm23 | 2884590 | sell | out | 1 | 106555 |
无论之前的交易如何,此时 Element 数组的大小为零,且没有任何未平仓头寸。2023年5月4日09:42:05,一笔1手的卖出入场交易被执行(该交易已被记录在平台的历史记录中),这立即触发了 OnTrade() 函数。考虑到 MetaTrader 5 在几分钟前(09:15)已在计算机上启动,date 变量有足够的时间更新为 2023.05.04 09:15:00,并且这个值一直存储在那里。在 OnTrade() 中,我们遍历未平仓头寸列表。我们使用的账户类型只允许每个交易品种持有一个头寸。在这种情况下,它是 WINM23。numberOfPositions 变量取值为 1,positionID 变量取值为 1352975,这与第一笔交易的单号一致,而该单号又是创建它的订单编号。现在,date 变量用交易的时间更新,所有未来的交易(直到交易编号 2193299)都将从 Identifier() 函数中获得相同的时间。
ListOrdersPositions(date) 函数被触发,并选择从 09:42:05 到 TimeCurrent() 的时间段以检索历史数据。在循环中,检测到入场类型为“IN”时,AddValue() 函数被调用,参数为:价格=104035,成交量=1,单号=1352975。由于 AddValue() 在最初为空的数组中未找到此单号,因此它插入了一行包含这三个值的新行。然后,ArrayPrint(Element) 函数在终端中显示这个矩阵。
接下来,PlotRectangles() 函数被调用,它保存了当前K线和第15根前K线的时间戳。这些值决定了要绘制的线的长度。GetDigits() 函数定义了该交易品种的最小变动单位的小数位数(在这种情况下为零),它被用来生成与 Element 数组中存储的价格值一起的对象名称。只要数组中对应的价格成交量不为零且图表上不存在这些对象,就会创建矩形和文本对象。如果对象已经存在,则会更新其属性,例如颜色、文本和位置。尽管这些矩形在技术上作为线条(因为它们没有高度)工作,但最初选择 OBJ_RECTANGLE 是为了在移除图表时能够删除所有这种类型的对象。尽管这种通用的删除机制从未实现,但零高度矩形的使用被保留了下来。因此,对应于买入交易 104035 的数组行被处理。由于其成交量不为零且名为“104035text”的对象尚不存在,因此创建了相关的文本和矩形对象。
在下一分钟,执行了一笔2手的卖出交易。由于买入仓位中已经存在1手,这导致仓位反转,留下一个1手的空头仓位。MetaTrader 立即将这笔交易添加到历史记录中。应用的处理逻辑与之前相同,遍历订单历史循环。带有单号 ticket=1352975 的交易再次出现在选定的时间段内,并被传递给 AddValue() 函数。由于该函数现在在数组中唯一的现有条目中找到了这个单号,因此它不会添加新的条目。接下来检测到的交易类型为“INOUT”,数组中唯一的现有交易将其 Element[0][1] 值存储在 newVolume 中,然后将其设置为零。
交易量计算为 HistoryDealGetDouble(ticket, DEAL_PRICE) - newVolume,且 newVolume = 2 - 1 = 1。因此,执行了 AddValue(103900, 1, 135370),遵循相同的逻辑。PlotRectangles() 函数再次运行,通过 Sort() 对数组进行升序排序后,数组中的第一个价格现在是 103900。由于图表上不存在此价格的对象,因此创建了它们。第二个数组元素(价格为 104035)已经绘制了其对象,因此更新了其属性。在这个阶段,Element 数组包含:{{103900,1,1356370}}, {104035,0,1352975}}。
随着过程的继续,出现了第三笔交易,被识别为退出交易,价格为 103700,成交量为 1,单号为 2193299。退出交易触发了带有这些参数的 RemoveValue() 函数。如果遇到零成交量或具有相同单号的现有行,RemoveValue() 将终止。由于这些条件未满足,函数继续使用 ArrayBsearch() 查找要移除的价格。它是一个二分查找算法,需要一个已排序的数组(由 Sort() 确保)。最接近 103700 的索引是数组中的第一个条目。由于这一行的成交量也是 1,因此将其置零,触发了 RemoveRectangle() 函数,该函数移除了与价格 103900 相关的图形对象。随后,AddValue() 插入了行 {103700,0,219299},它不会被 Sort() 改变。仓位现在已关闭。在这个阶段,Element 数组包含:{{103700,0,219299}}, {103900,0,1356370}}, {104035,0,1352975}}。
当一个仓位完全关闭时,numberOfPositions 变量被设置为零,当 ClearRectangles() 成功执行时,isClean 变量被设置为 true。数组被清空,date 更新为当前时间。这意味着在新定义的期间内不会返回任何订单。系统等待新的交易,以便继续传递到数组并处理后续动作。在这个阶段,Element 数组为空:{}。这使系统回到了类似于本示例开头描述的状态。相同的逻辑可以应用于理解后续交易中的指标行为。在当前示例中,操作从价格 103690 开始,如“5. 实际使用示例”中引用的。用法实例通过仔细跟随每一步,可以清楚地了解为什么在第一个示例中描述的行为会发生。解释与退出交易价格有关,以及算法如何依次移除与“DEAL_ENTRY_OUT”交易价格最接近的行。
8. 集成到EA中
有两种方法可以在策略测试器中使用这种自定义指标。第一种方法是编译一个EA或另一个调用自定义指标的指标。为此,请确保编译文件“Plotagem de Entradas Parciais.ex5”位于“Indicators”文件夹中。然后,将以下代码行插入调用者的 OnInit() 函数中。在执行此操作之前,请记住声明全局变量 handlePlotagemEntradasParciais 为 int 类型:
iCustom(_Symbol,PERIOD_CURRENT,"Plotagem de Entradas Parciais"); //--- if the handle is not created if(handlePlotagemEntradasParciais ==INVALID_HANDLE) { //--- Print an error message and exit with an error code PrintFormat("Failed to create indicator handle for symbol %s/%s, error code %d", _Symbol, EnumToString(_Period), GetLastError()); //--- The indicator is terminated prematurely return(INIT_FAILED); }
第二种方法消除了在EA中修改这些代码行的必要性,使其成为测试时更便捷的选择。只需使用标准方法将指标加载到图表上,然后将模板保存为“Tester.tpl”(如有必要,可覆盖同名的现有文件)。这确保了每次测试EA时,指标都会自动加载。请注意,此方法仅在策略测试器中启用了带有图表显示的可视化模式时才相关。
9. 结论
我们创建了一个自定义指标,用于绘制部分入场交易,以探索在MQL5中创建和利用指标的新方法。MQL5是MetaTrader 5这一领先交易平台所使用的最先进和现代的编程语言之一。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/12576


