如何不通过翻找历史交易记录直接在图表上查看交易情况
内容
引言
现代交易者经常面临分析大量与交易操作相关的数据的问题。在MetaTrader 5客户端终端中,由于图表上充斥着开仓和平仓的标签,交易历史的显示可能会变得无效且令人困惑。这对于处理大量交易的交易者来说尤其如此,因为图表会迅速填满,使得几乎无法分析交易活动并做出明智的决策。
本文的目的是提供一种解决方案,使人们能够更轻松地了解和分析交易历史。我们将开发一个机制,用于逐步展示已平仓头寸并改进交易信息的显示方式。这将使交易者能够专注于每一笔交易,并更深入地了解他们的交易操作。
我们将实现以下功能:
- 使用导航键逐一查看已平仓头寸。
- 通过提供有关每笔交易的更详细信息来改进提示条。
- 将图表置于中央,以便最重要的元素始终可见。
此外,我们将详细研究实现这些功能所需的数据结构,并提出MetaTrader 5中交易和持仓核算的基本原则。因此,交易者将能够更有效地管理其交易历史,并根据收到的信息做出明智的决策。
在MetaTrader 5客户端中,我们可以通过在图表设置(F8键)的“显示”选项卡中勾选“显示交易历史”选项来显示交易历史。
通过选中该选项,我们允许终端以标签的形式在图表上显示整个交易历史,这些标签表示开盘/收盘头寸,并通过线条连接。在这种情况下,将完整显示交易品种上执行的所有交易:
如果有很多交易,图表上就会布满标签,导致很难看到图表上的任何东西。当将鼠标悬停在交易标签或连接交易的线条上时出现的提示条并不是特别有用:
此处介绍的解决方案将在启动时仅显示最后一个已平仓的头寸。通过按光标键可以浏览其余头寸:
- 向上键将显示第一个平仓头寸;
- 向下键将显示最后一个平仓头寸;
- 左键显示先前的平仓头寸;
- 右箭头键将显示下一个已平仓头寸。
交易和连接线的提示条将显示更多有用信息。此外,当按住Shift键时,图表上将显示当前选定的已平仓头寸的信息:
在显示每个后续已平仓头寸时,我们将把图表居中,以便头寸开盘/收盘标签和连接它们的线条位于中心。
如果开盘价和收盘价标签无法在一个图表屏幕内容纳,则将图表置于中央,以便开仓交易位于图表左侧第二个可见柱上。
让我们来看看交易和仓单核算基本原则,以及我们将在这里创建的类的结构。
仓单是如何形成的?首先,向服务器发送交易请求——订单(交易订单)。订单可以被取消或执行。订单执行后,我们会收到一笔交易 - 交易请求已执行的事实。如果在执行订单时没有头寸,则该交易会在某个方向上创建一个头寸。如果当时有头寸,则根据头寸核算类型有几种选项。在净额核算的情况下,一个交易品种只能有一个头寸。因此,由交易订单产生的交易会修改现有的头寸。它可以是:
- 平仓——对于多头头寸,如果执行了等于当前头寸交易量的卖出:头寸 1.0 买入 - 交易 1.0 卖出 = 交易量 0(平仓);
- 部分平仓——对于多头头寸,如果执行了小于当前头寸交易量的卖出:
头寸 1.0 买入 - 交易 0.5 卖出 = 交易量 0.5(部分平仓); - 加仓——对于多头头寸,如果执行了买入:
头寸 1.0 买入 + 交易 0.5 买入 = 交易量 1.5(增加头寸交易量); - 反转——对于多头头寸,如果执行了大于头寸交易量的卖出:
头寸 1.0 买入 - 交易 1.5 卖出 = 头寸 0.5 卖出(头寸反转);
在套期保值账户类型的情况下,每笔交易要么修改现有头寸,要么生成新头寸。要更改现有头寸(平仓或部分平仓),我们应在交易订单中指定要与其进行交易操作的头寸的ID。头寸ID是为每个新头寸分配的唯一编号,在头寸生命周期内保持不变:
- 开立新的头寸——在已有头寸的情况下,执行买入或卖出:
头寸1.0买入 + 交易1.0卖出 = 2个独立头寸:1.0买入和1.0卖出(开仓),或
头寸1.0买入 + 交易1.5买入 = 2个独立头寸:1.0买入和1.5买入(开仓),等等。 - 平仓——对已存在的多头头寸执行卖出操作:
头寸1.0买入(ID为123) - 交易1.0卖出(ID为123) = 平掉ID为123的1.0买入头寸(平掉ID为123的头寸); - 部分平仓——对已存在的多头头寸执行小于该头寸指定ID的交易量的卖出操作:
头寸1.0买入(ID为123) - 交易0.5卖出(ID为123) = 剩余ID为123的0.5买入头寸(部分平掉ID为123的头寸);
了解更多关于订单、交易和头寸的信息,请参阅文章“MetaTrader 5中的订单、头寸和交易”。
发送到交易服务器的每个订单都会保留在终端中的活动订单列表中,直到其被执行。当订单被触发时,会出现一笔交易,从而产生一个新的头寸或改变现有的头寸。在订单执行时,它会被放入历史订单列表中,并创建一个交易,该交易也会被放入历史交易列表中。如果交易创建了头寸,则会创建一个新的头寸并将其放入活跃头寸列表中。
终端中没有历史(已平仓)头寸的列表。同样,终端中也没有活跃交易的列表——它们一旦完成就被视为历史交易,并位于相应的列表中。
原因很简单:订单是执行账户交易操作的指令它一直存在,直到被完成。它位于现有订单列表中。一旦订单被执行,它就不再存在(理想情况下)并被放置在已执行交易订单的列表中。在此过程中,它会创建一笔交易。交易是执行交易订单的事实。这是一个一次性事件:订单被执行 - 我们有一笔交易。仅此而已。交易被添加到已完成的账户事件列表中。
交易创建一个头寸,该头寸一直存在,直到被平仓。头寸始终位于活跃头寸列表中。一旦平仓,它就从列表中移除。
终端中没有已平仓头寸的列表。但有一个已完成的交易列表。因此,为了重新创建已平仓头寸列表,我们需要根据属于先前已平仓头寸的交易来重构它们。交易列表始终对我们可用。每个交易的属性中包含相应头寸的ID。因此,我们拥有重新创建账户上先前存在的每个历史头寸头寸所需的一切信息——头寸ID和交易列表。
基于上述内容,我们得到以下从现有历史交易列表中重新创建先前已平仓头寸的算法:
- 获取所需交易品种的所有交易列表;
- 遍历列表并从列表中获取下一笔交易;
- 检查交易属性中的头寸ID;
- 如果我们的历史头寸列表中不存在具有此ID的头寸,则创建一个新的历史头寸对象;
- 使用先前收到的ID获取历史头寸对象;
- 将当前交易添加到历史头寸对象的交易列表中。
在遍历终端交易列表完成后,我们将拥有按品种分类的账户上所有已平仓头寸的完整列表,其中包含属于每个头寸的交易列表。
现在,每个历史头寸都将包含涉及改变该头寸的交易列表,我们将能够获得关于已平仓头寸在其存在期间所有变化的完整信息。
要实现上述逻辑,我们需要创建三个类:
- 交易类。该类将包含历史交易的所有属性列表、用于访问属性的方法(设置和获取),以及用于显示交易数据的附加方法;
- 头寸类。该类将包含所有交易的列表、用于访问它们的方法以及用于显示头寸及其交易信息的附加方法;
- 历史头寸管理类。该类将包含所有历史头寸的列表、创建和更新该列表的方法,以及用于访问头寸及其交易属性的方法,还有用于显示头寸及其交易信息的附加方法。
我们开始吧。
交易类
为了获取并保存交易的属性,我们需要选择订单和交易的历史记录(HistorySelect()),然后遍历交易历史记录,并通过循环索引从历史交易列表中获取交易的编号(HistoryDealGetTicket())。在这种情况下,某笔交易将被选中,然后使用HistoryDealGetInteger()、HistoryDealGetDouble()和HistoryDealGetString()函数获取其属性。
交易类中,我们假设交易已经被选中,并且可以立即获取其属性。除了记录和检索交易属性外,该类还允许我们在图表上创建一个图形标签来显示交易。标签将在画布上绘制,这样我们就可以为每种类型的交易绘制我们想要的标签,而不是使用系统预定义的字体、标识符集中的标签。在创建每个交易的对象时,我们将接收交易出现时的价格变动历史,从而允许我们计算交易执行时的点差,并在交易参数描述中显示它。
由于可能有多笔交易参与影响每个头寸,因此所有交易最终都将位于指向CObject类实例及其CArrayObj衍生类的动态数组类中。
因此,交易类将从标准库的CObject基类继承。
交易具有一组整数、实数和字符串属性。每个交易对象都将在历史头寸类的对象列表中。为了实现对所需交易的搜索,需要编写参数枚举值。使用此枚举值,可以在列表中搜索交易并按这些相同的值对列表进行排序。我们只需要通过属性进行搜索——交易的编号和以毫秒为单位的时间。交易编号将允许我们确定该交易是否已存在于列表中。以毫秒为单位的时间将使我们能够严格按照时间顺序排列交易。由于类是可扩展的,因此交易属性的列表将包含其所有属性。
为了在标准库CArrayObj列表中组织搜索和排序,CObject类中的Compare()虚方法返回值0:
//--- method of comparing the objects virtual int Compare(const CObject *node,const int mode=0) const { return(0); }
换句话说,此方法总是返回相等。在从CObject继承的每个类的对象中,都应该重新定义此方法,以便在比较两个对象的属性(在mode参数中指定)时,该方法返回:
- -1 — 如果当前对象属性的值小于比较对象的值;
- 1 — 如果当前对象属性的值大于比较对象的值;
- 0 — 如果两个比较对象的指定属性值相等。
因此,根据上面的解释,对于今天创建的每个类,我们还需要额外创建类对象属性的枚举和比较它们的方法。
在\MQL5\Include\文件夹中,创建包含CDeal类的新文件PositionsViewerDeal.mqh的PositionsViewer文件夹:
标准库的CObject类应该是基类:
由此,我们将获得交易对象类文件的以下模板:
//+------------------------------------------------------------------+ //| Deal.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" class CDeal : public CObject { private: public: CDeal(); ~CDeal(); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CDeal::CDeal() { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CDeal::~CDeal() { } //+------------------------------------------------------------------+
将CObject库的标准对象类和CCanvas类文件包含到创建的交易对象类文件中,并编写用于按交易对象属性排序的枚举:
//+------------------------------------------------------------------+ //| Deal.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include <Object.mqh> #include <Canvas\Canvas.mqh> enum ENUM_DEAL_SORT_MODE { SORT_MODE_DEAL_TICKET = 0, // Mode of comparing/sorting by a deal ticket SORT_MODE_DEAL_ORDER, // Mode of comparing/sorting by the order a deal is based on SORT_MODE_DEAL_TIME, // Mode of comparing/sorting by a deal time SORT_MODE_DEAL_TIME_MSC, // Mode of comparing/sorting by a deal time in milliseconds SORT_MODE_DEAL_TYPE, // Mode of comparing/sorting by a deal type SORT_MODE_DEAL_ENTRY, // Mode of comparing/sorting by a deal direction SORT_MODE_DEAL_MAGIC, // Mode of comparing/sorting by a deal magic number SORT_MODE_DEAL_REASON, // Mode of comparing/sorting by a deal reason or source SORT_MODE_DEAL_POSITION_ID, // Mode of comparing/sorting by a position ID SORT_MODE_DEAL_VOLUME, // Mode of comparing/sorting by a deal volume SORT_MODE_DEAL_PRICE, // Mode of comparing/sorting by a deal price SORT_MODE_DEAL_COMMISSION, // Mode of comparing/sorting by commission SORT_MODE_DEAL_SWAP, // Mode of comparing/sorting by accumulated swap on close SORT_MODE_DEAL_PROFIT, // Mode of comparing/sorting by a deal financial result SORT_MODE_DEAL_FEE, // Mode of comparing/sorting by a deal fee SORT_MODE_DEAL_SL, // Mode of comparing/sorting by Stop Loss level SORT_MODE_DEAL_TP, // Mode of comparing/sorting by Take Profit level SORT_MODE_DEAL_SYMBOL, // Mode of comparing/sorting by a name of a traded symbol SORT_MODE_DEAL_COMMENT, // Mode of comparing/sorting by a deal comment SORT_MODE_DEAL_EXTERNAL_ID, // Mode of comparing/sorting by a deal ID in an external trading system }; //+------------------------------------------------------------------+ //| Deal class | //+------------------------------------------------------------------+ class CDeal : public CObject { }
将类正常运作所需的所有变量和方法分别声明在私有(private)、受保护(protected)和公有(public)部分中:
//+------------------------------------------------------------------+ //| Deal class | //+------------------------------------------------------------------+ class CDeal : public CObject { private: MqlTick m_tick; // Deal tick structure //--- CCanvas object CCanvas m_canvas; // Canvas long m_chart_id; // Chart ID int m_width; // Canvas width int m_height; // Canvas height string m_name; // Graphical object name //--- Create a label object on the chart bool CreateLabelObj(void); //--- Draw (1) a mask, (2) Buy arrow on the canvas void DrawArrowMaskBuy(const int shift_x, const int shift_y); void DrawArrowBuy(const int shift_x, const int shift_y); //--- Draw (1) a mask, (2) Sell arrow on the canvas void DrawArrowMaskSell(const int shift_x, const int shift_y); void DrawArrowSell(const int shift_x, const int shift_y); //--- Draw the label appearance void DrawLabelView(void); //--- Get a (1) deal tick and (2) a spread of the deal minute bar bool GetDealTick(const int amount=20); int GetSpreadM1(void); //--- Return time with milliseconds string TimeMscToString(const long time_msc,int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const; protected: //--- Integer properties long m_ticket; // Deal ticket. Unique number assigned to each deal long m_order; // Deal order number datetime m_time; // Deal execution time long m_time_msc; // Deal execution time in milliseconds since 01.01.1970 ENUM_DEAL_TYPE m_type; // Deal type ENUM_DEAL_ENTRY m_entry; // Deal entry - entry in, entry out, reverse long m_magic; // Magic number for a deal (see ORDER_MAGIC) ENUM_DEAL_REASON m_reason; // Deal execution reason or source long m_position_id; // The ID of the position opened, modified or closed by the deal //--- Real properties double m_volume; // Deal volume double m_price; // Deal price double m_commission; // Deal commission double m_swap; // Accumulated swap when closing double m_profit; // Deal financial result double m_fee; // Fee for making a deal charged immediately after performing a deal double m_sl; // Stop Loss level double m_tp; // Take Profit level //--- String properties string m_symbol; // Name of the symbol for which the deal is executed string m_comment; // Deal comment string m_external_id; // Deal ID in an external trading system (on the exchange) //--- Additional properties int m_digits; // Symbol digits double m_point; // Symbol point double m_bid; // Bid when performing a deal double m_ask; // Ask when performing a deal int m_spread; // Spread when performing a deal color m_color_arrow; // Deal label color //--- Draws an arrow corresponding to the deal type. It can be redefined in the inherited classes virtual void DrawArrow(void); public: //--- Set the properties //--- Integer properties void SetTicket(const long ticket) { this.m_ticket=ticket; } // Ticket void SetOrder(const long order) { this.m_order=order; } // Order void SetTime(const datetime time) { this.m_time=time; } // Time void SetTimeMsc(const long value) { this.m_time_msc=value; } // Time in milliseconds void SetTypeDeal(const ENUM_DEAL_TYPE type) { this.m_type=type; } // Type void SetEntry(const ENUM_DEAL_ENTRY entry) { this.m_entry=entry; } // Direction void SetMagic(const long magic) { this.m_magic=magic; } // Magic number void SetReason(const ENUM_DEAL_REASON reason) { this.m_reason=reason; } // Deal execution reason or source void SetPositionID(const long id) { this.m_position_id=id; } // Position ID //--- Real properties void SetVolume(const double volume) { this.m_volume=volume; } // Volume void SetPrice(const double price) { this.m_price=price; } // Price void SetCommission(const double value) { this.m_commission=value; } // Commission void SetSwap(const double value) { this.m_swap=value; } // Accumulated swap when closing void SetProfit(const double value) { this.m_profit=value; } // Financial result void SetFee(const double value) { this.m_fee=value; } // Deal fee void SetSL(const double value) { this.m_sl=value; } // Stop Loss level void SetTP(const double value) { this.m_tp=value; } // Take Profit level //--- String properties void SetSymbol(const string symbol) { this.m_symbol=symbol; } // Symbol name void SetComment(const string comment) { this.m_comment=comment; } // Comment void SetExternalID(const string ext_id) { this.m_external_id=ext_id; } // Deal ID in an external trading system //--- Get the properties //--- Integer properties long Ticket(void) const { return(this.m_ticket); } // Ticket long Order(void) const { return(this.m_order); } // Order datetime Time(void) const { return(this.m_time); } // Time long TimeMsc(void) const { return(this.m_time_msc); } // Time in milliseconds ENUM_DEAL_TYPE TypeDeal(void) const { return(this.m_type); } // Type ENUM_DEAL_ENTRY Entry(void) const { return(this.m_entry); } // Direction long Magic(void) const { return(this.m_magic); } // Magic number ENUM_DEAL_REASON Reason(void) const { return(this.m_reason); } // Deal execution reason or source long PositionID(void) const { return(this.m_position_id); } // Position ID //--- Real properties double Volume(void) const { return(this.m_volume); } // Volume double Price(void) const { return(this.m_price); } // Price double Commission(void) const { return(this.m_commission); } // Commission double Swap(void) const { return(this.m_swap); } // Accumulated swap when closing double Profit(void) const { return(this.m_profit); } // Financial result double Fee(void) const { return(this.m_fee); } // Deal fee double SL(void) const { return(this.m_sl); } // Stop Loss level double TP(void) const { return(this.m_tp); } // Take Profit level double Bid(void) const { return(this.m_bid); } // Bid when performing a deal double Ask(void) const { return(this.m_ask); } // Ask when performing a deal int Spread(void) const { return(this.m_spread); } // Spread when performing a deal //--- String properties string Symbol(void) const { return(this.m_symbol); } // Symbol name string Comment(void) const { return(this.m_comment); } // Comment string ExternalID(void) const { return(this.m_external_id); } // Deal ID in an external trading system //--- Set the color of the deal label void SetColorArrow(const color clr); //--- (1) Hide, (2) display the deal label on a chart void HideArrow(const bool chart_redraw=false); void ShowArrow(const bool chart_redraw=false); //--- Return the description of a (1) deal type, (2) position change method and (3) deal reason string TypeDescription(void) const; string EntryDescription(void) const; string ReasonDescription(void) const; //--- Return (1) a short description and (2) a tooltip text of a deal string Description(void); virtual string Tooltip(void); //--- Print deal properties in the journal void Print(void); //--- Compare two objects by the property specified in 'mode' virtual int Compare(const CObject *node, const int mode=0) const; //--- Constructors/destructor CDeal(void) { this.m_ticket=0; } CDeal(const ulong ticket); ~CDeal(); };
我们来详细分析所声明的方法。
参数型构造函数:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CDeal::CDeal(const ulong ticket) { //--- Store the properties //--- Integer properties this.m_ticket = (long)ticket; // Deal ticket this.m_order = ::HistoryDealGetInteger(ticket, DEAL_ORDER); // Order this.m_time = (datetime)::HistoryDealGetInteger(ticket, DEAL_TIME); // Deal execution time this.m_time_msc = ::HistoryDealGetInteger(ticket, DEAL_TIME_MSC); // Deal execution time in milliseconds this.m_type = (ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE); // Type this.m_entry = (ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket, DEAL_ENTRY); // Direction this.m_magic = ::HistoryDealGetInteger(ticket, DEAL_MAGIC); // Magic number this.m_reason = (ENUM_DEAL_REASON)::HistoryDealGetInteger(ticket, DEAL_REASON); // Deal execution reason or source this.m_position_id= ::HistoryDealGetInteger(ticket, DEAL_POSITION_ID); // Position ID //--- Real properties this.m_volume = ::HistoryDealGetDouble(ticket, DEAL_VOLUME); // Volume this.m_price = ::HistoryDealGetDouble(ticket, DEAL_PRICE); // Price this.m_commission = ::HistoryDealGetDouble(ticket, DEAL_COMMISSION); // Commission this.m_swap = ::HistoryDealGetDouble(ticket, DEAL_SWAP); // Accumulated swap when closing this.m_profit = ::HistoryDealGetDouble(ticket, DEAL_PROFIT); // Financial result this.m_fee = ::HistoryDealGetDouble(ticket, DEAL_FEE); // Deal fee this.m_sl = ::HistoryDealGetDouble(ticket, DEAL_SL); // Stop Loss level this.m_tp = ::HistoryDealGetDouble(ticket, DEAL_TP); // Take Profit level //--- String properties this.m_symbol = ::HistoryDealGetString(ticket, DEAL_SYMBOL); // Symbol name this.m_comment = ::HistoryDealGetString(ticket, DEAL_COMMENT); // Comment this.m_external_id= ::HistoryDealGetString(ticket, DEAL_EXTERNAL_ID); // Deal ID in an external trading system //--- Graphics display parameters this.m_chart_id = ::ChartID(); this.m_digits = (int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS); this.m_point = ::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT); this.m_width = 19; this.m_height = 19; this.m_name = "Deal#"+(string)this.m_ticket; this.m_color_arrow= (this.TypeDeal()==DEAL_TYPE_BUY ? C'3,95,172' : this.TypeDeal()==DEAL_TYPE_SELL ? C'225,68,29' : C'180,180,180'); //--- Parameters for calculating spread this.m_spread = 0; this.m_bid = 0; this.m_ask = 0; //--- Create a graphic label this.CreateLabelObj(); //--- If the historical tick and the Point value of the symbol were obtained if(this.GetDealTick() && this.m_point!=0) { //--- set the Bid and Ask price values, calculate and save the spread value this.m_bid=this.m_tick.bid; this.m_ask=this.m_tick.ask; this.m_spread=int((this.m_ask-this.m_bid)/this.m_point); } //--- If failed to obtain a historical tick, take the spread value of the minute bar the deal took place on else this.m_spread=this.GetSpreadM1(); }
请记住,当前交易已经被选中。因此,我们可以立即从交易属性中填充所有对象的属性。接下来,设置用于在图表上显示交易标签的画布参数,创建这个标签的图形对象,并通过交易发生的时间获取行情数据(tick),以计算交易发生时的点差。如果无法获取行情数据,则取交易发生所在分钟线行情的平均点差。
结果,在创建交易对象时,我们立即获得了一个对象,该对象根据交易单据设置了历史交易属性,以及一个已创建但隐藏的用于在图表上可视化交易的标签,还有交易发生时的点差值。
在类的析构函数中,检查反初始化的原因。如果不是因为图表时间框架的更改,则销毁图形标签对象的图形资源:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CDeal::~CDeal() { if(::UninitializeReason()!=REASON_CHARTCHANGE) this.m_canvas.Destroy(); }
通过指定属性比较两个对象的虚拟方法:
//+------------------------------------------------------------------+ //| Compare two objects by the specified property | //+------------------------------------------------------------------+ int CDeal::Compare(const CObject *node,const int mode=0) const { const CDeal * obj = node; switch(mode) { case SORT_MODE_DEAL_TICKET : return(this.Ticket() > obj.Ticket() ? 1 : this.Ticket() < obj.Ticket() ? -1 : 0); case SORT_MODE_DEAL_ORDER : return(this.Order() > obj.Order() ? 1 : this.Order() < obj.Order() ? -1 : 0); case SORT_MODE_DEAL_TIME : return(this.Time() > obj.Time() ? 1 : this.Time() < obj.Time() ? -1 : 0); case SORT_MODE_DEAL_TIME_MSC : return(this.TimeMsc() > obj.TimeMsc() ? 1 : this.TimeMsc() < obj.TimeMsc() ? -1 : 0); case SORT_MODE_DEAL_TYPE : return(this.TypeDeal() > obj.TypeDeal() ? 1 : this.TypeDeal() < obj.TypeDeal() ? -1 : 0); case SORT_MODE_DEAL_ENTRY : return(this.Entry() > obj.Entry() ? 1 : this.Entry() < obj.Entry() ? -1 : 0); case SORT_MODE_DEAL_MAGIC : return(this.Magic() > obj.Magic() ? 1 : this.Magic() < obj.Magic() ? -1 : 0); case SORT_MODE_DEAL_REASON : return(this.Reason() > obj.Reason() ? 1 : this.Reason() < obj.Reason() ? -1 : 0); case SORT_MODE_DEAL_POSITION_ID : return(this.PositionID() > obj.PositionID() ? 1 : this.PositionID() < obj.PositionID() ? -1 : 0); case SORT_MODE_DEAL_VOLUME : return(this.Volume() > obj.Volume() ? 1 : this.Volume() < obj.Volume() ? -1 : 0); case SORT_MODE_DEAL_PRICE : return(this.Price() > obj.Price() ? 1 : this.Price() < obj.Price() ? -1 : 0); case SORT_MODE_DEAL_COMMISSION : return(this.Commission() > obj.Commission() ? 1 : this.Commission() < obj.Commission() ? -1 : 0); case SORT_MODE_DEAL_SWAP : return(this.Swap() > obj.Swap() ? 1 : this.Swap() < obj.Swap() ? -1 : 0); case SORT_MODE_DEAL_PROFIT : return(this.Profit() > obj.Profit() ? 1 : this.Profit() < obj.Profit() ? -1 : 0); case SORT_MODE_DEAL_FEE : return(this.Fee() > obj.Fee() ? 1 : this.Fee() < obj.Fee() ? -1 : 0); case SORT_MODE_DEAL_SL : return(this.SL() > obj.SL() ? 1 : this.SL() < obj.SL() ? -1 : 0); case SORT_MODE_DEAL_TP : return(this.TP() > obj.TP() ? 1 : this.TP() < obj.TP() ? -1 : 0); case SORT_MODE_DEAL_SYMBOL : return(this.Symbol() > obj.Symbol() ? 1 : this.Symbol() < obj.Symbol() ? -1 : 0); case SORT_MODE_DEAL_COMMENT : return(this.Comment() > obj.Comment() ? 1 : this.Comment() < obj.Comment() ? -1 : 0); case SORT_MODE_DEAL_EXTERNAL_ID : return(this.ExternalID() > obj.ExternalID() ? 1 : this.ExternalID() < obj.ExternalID() ? -1 : 0); default : return(-1); } }
该方法接收指向比较对象的指针以及来自ENUM_DEAL_SORT_MODE枚举的比较属性值。如果当前对象的指定属性值大于比较对象的属性值,则返回1。如果当前对象的属性值小于比较对象的属性值,则返回-1。在任何其他情况下,返回0。
返回交易类型描述的方法:
//+------------------------------------------------------------------+ //| Return the deal type description | //+------------------------------------------------------------------+ string CDeal::TypeDescription(void) const { switch(this.m_type) { case DEAL_TYPE_BUY : return "Buy"; case DEAL_TYPE_SELL : return "Sell"; case DEAL_TYPE_BALANCE : return "Balance"; case DEAL_TYPE_CREDIT : return "Credit"; case DEAL_TYPE_CHARGE : return "Additional charge"; case DEAL_TYPE_CORRECTION : return "Correction"; case DEAL_TYPE_BONUS : return "Bonus"; case DEAL_TYPE_COMMISSION : return "Additional commission"; case DEAL_TYPE_COMMISSION_DAILY : return "Daily commission"; case DEAL_TYPE_COMMISSION_MONTHLY : return "Monthly commission"; case DEAL_TYPE_COMMISSION_AGENT_DAILY : return "Daily agent commission"; case DEAL_TYPE_COMMISSION_AGENT_MONTHLY: return "Monthly agent commission"; case DEAL_TYPE_INTEREST : return "Interest rate"; case DEAL_TYPE_BUY_CANCELED : return "Canceled buy deal"; case DEAL_TYPE_SELL_CANCELED : return "Canceled sell deal"; case DEAL_DIVIDEND : return "Dividend operations"; case DEAL_DIVIDEND_FRANKED : return "Franked (non-taxable) dividend operations"; case DEAL_TAX : return "Tax charges"; default : return "Unknown: "+(string)this.m_type; } }
所有交易类型的描述都在这里返回。其中大部分在程序中不会被使用。但该方法会返回所有类型,考虑到类的扩展和继承的可能性。
返回头寸变更方法描述的方法:
//+------------------------------------------------------------------+ //| Return position change method | //+------------------------------------------------------------------+ string CDeal::EntryDescription(void) const { switch(this.m_entry) { case DEAL_ENTRY_IN : return "Entry In"; case DEAL_ENTRY_OUT : return "Entry Out"; case DEAL_ENTRY_INOUT : return "Reverse"; case DEAL_ENTRY_OUT_BY : return "Close a position by an opposite one"; default : return "Unknown: "+(string)this.m_entry; } }
返回交易原因描述的方法:
//+------------------------------------------------------------------+ //| Return a deal reason description | //+------------------------------------------------------------------+ string CDeal::ReasonDescription(void) const { switch(this.m_reason) { case DEAL_REASON_CLIENT : return "Terminal"; case DEAL_REASON_MOBILE : return "Mobile"; case DEAL_REASON_WEB : return "Web"; case DEAL_REASON_EXPERT : return "EA"; case DEAL_REASON_SL : return "SL"; case DEAL_REASON_TP : return "TP"; case DEAL_REASON_SO : return "SO"; case DEAL_REASON_ROLLOVER : return "Rollover"; case DEAL_REASON_VMARGIN : return "Var. Margin"; case DEAL_REASON_SPLIT : return "Split"; case DEAL_REASON_CORPORATE_ACTION: return "Corp. Action"; default : return "Unknown reason "+(string)this.m_reason; } }
我们将仅仅使用方法返回的三个值:止损(StopLoss),止盈(TakeProfit)和强制平仓(StopOut)。
返回交易描述的方法:
//+------------------------------------------------------------------+ //| Return deal description | //+------------------------------------------------------------------+ string CDeal::Description(void) { return(::StringFormat("Deal: %-9s %.2f %-4s #%I64d at %s", this.EntryDescription(), this.Volume(), this.TypeDescription(), this.Ticket(), this.TimeMscToString(this.TimeMsc()))); }
使用 StringFormat() 函数来创建并返回一个特定的字符串:
Deal: Entry In 0.10 Buy #1728374638 at 2023.06.12 16:51:36.838
返回交易弹出消息文本的虚拟方法:
//+------------------------------------------------------------------+ //| Returns a text of a deal pop-up message | //+------------------------------------------------------------------+ string CDeal::Tooltip(void) { return(::StringFormat("Position ID #%I64d %s:\nDeal #%I64d %.2f %s %s\n%s [%.*f]\nProfit: %.2f, SL: %.*f, TP: %.*f", this.PositionID(), this.Symbol(), this.Ticket(), this.Volume(), this.TypeDescription(), this.EntryDescription(), this.TimeMscToString(this.TimeMsc()), this.m_digits, this.Price(), this.Profit(), this.m_digits, this.SL(), this.m_digits, this.TP())); }
与上述方法类似,这里会形成一个特定类型的文本字符串并返回:
Position ID #1752955040 EURUSD: Deal #1728430603 0.10 Sell Entry Out 2023.06.12 17:04:20.362 [1.07590] Profit: 15.00, SL: 1.07290, TP: 1.07590
此字符串稍后将用于在鼠标指针悬停在图表的交易标签上时,在提示条中显示交易描述。可以在继承类中重新定义此方法,以输出其他交易信息。
在日志中打印交易属性的方法:
//+------------------------------------------------------------------+ //| Print deal properties in the journal | //+------------------------------------------------------------------+ void CDeal::Print(void) { ::PrintFormat(" %s", this.Description()); }
该方法不会打印所有的交易属性,而只是显示上述讨论的Description()方法返回的交易的最简信息。在显示头寸描述时,会在交易描述字符串前添加两个空格,以便整理信息,其中仓位描述标题后面跟着头寸交易的数据。
Position EURUSD 0.10 Buy #1752955040, Magic 0 -Opened 2023.06.12 16:51:36.838 [1.07440] -Closed 2023.06.12 17:04:20.362 [1.07590] Deal: Entry In 0.10 Buy #1728374638 at 2023.06.12 16:51:36.838 Deal: Entry Out 0.10 Sell #1728430603 at 2023.06.12 17:04:20.362
想了解更多关于PrintFormat()和StringFormat()函数的信息,可以查阅文章“研究PrintFormat()并应用现成的示例”和“StringFormat()的现成示例和回顾”。
返回带毫秒的时间的方法:
//+------------------------------------------------------------------+ //| Return time with milliseconds | //+------------------------------------------------------------------+ string CDeal::TimeMscToString(const long time_msc, int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const { return(::TimeToString(time_msc/1000, flags) + "." + ::IntegerToString(time_msc %1000, 3, '0')); }
在这里,我们构造了一个字符串,该字符串包含通过将毫秒值时间除以1000转换成的正常时间值。将毫秒时间除以1000所得的余数(即毫秒数)添加到一个点(小数点)之后的字符串结果中。毫秒字符串被格式化为一个三位数,如果其位数少于三位,则在前面添加零。因此,我们得到了以下时间表示形式:
2023.06.12 17:04:20.362
为了了解交易发生时的点差是多少,需要获取交易时间(以毫秒为单位)所对应的必要价格变动(tick)。使用标准的CopyTicks()函数在已知时间复制一个价格变动这一看似简单的任务,却引发了一场寻找解决方案的小小探索,因为无法精确复制指定时间点的价格变动。经过一番寻找解决方案的努力后,我终于找到了必要的算法:我们需要在不断扩大的时间范围“From”到交易时间之间发出一定数量的价格变动请求。更多相关信息请见此处(俄语)。
获取订单价格变动的方法:
//+------------------------------------------------------------------+ //| Get the deal tick | //+------------------------------------------------------------------+ bool CDeal::GetDealTick(const int amount=20) { MqlTick ticks[]; // We will receive ticks here int attempts = amount; // Number of attempts to get ticks int offset = 500; // Initial time offset for an attempt int copied = 0; // Number of ticks copied //--- Until the tick is copied and the number of copy attempts is over //--- we try to get a tick, doubling the initial time offset at each iteration (expand the "from_msc" time range) while(!::IsStopped() && (copied<=0) && (attempts--)!=0) copied = ::CopyTicksRange(this.m_symbol, ticks, COPY_TICKS_INFO, this.m_time_msc-(offset <<=1), this.m_time_msc); //--- If the tick was successfully copied (it is the last one in the tick array), set it to the m_tick variable if(copied>0) this.m_tick=ticks[copied-1]; //--- Return the flag that the tick was copied return(copied>0); }
在接收到一个价格变动(tick)后,会从其中获取卖出价(Ask)和买入价(Bid),并计算点差大小,公式为(卖出价 - 买入价)/ 点值(Point)。
如果无法使用此方法获得价格变动,则可以使用获取交易1分钟K线点差的方法来计算点差的平均值:
//+------------------------------------------------------------------+ //| Gets the spread of the deal minute bar | //+------------------------------------------------------------------+ int CDeal::GetSpreadM1(void) { int array[1]={}; int bar=::iBarShift(this.m_symbol, PERIOD_M1, this.Time()); if(bar==WRONG_VALUE) return 0; return(::CopySpread(this.m_symbol, PERIOD_M1, bar, 1, array)==1 ?array[0] : 0); }
在这里,我们通过交易时间获取1分钟K线的开盘时间,然后使用CopySpread()函数获取该柱的平均点差。如果获取柱或点差时出错,该方法将返回零。
在图表上创建标签对象的方法:
//+------------------------------------------------------------------+ //| Create a label object on the chart | //+------------------------------------------------------------------+ bool CDeal::CreateLabelObj(void) { //--- Create a graphical resource with a Bitmap object attached to it ::ResetLastError(); if(!this.m_canvas.CreateBitmap(this.m_name, this.m_time, this.m_price, this.m_width, this.m_height, COLOR_FORMAT_ARGB_NORMALIZE)) { ::PrintFormat("%s: When creating a graphic object, error %d occurred in the CreateBitmap method of the CCanvas class",__FUNCTION__, ::GetLastError()); return false; } //--- If the graphical resource is successfully created, set the Bitmap object, anchor point, price, time and tooltip text ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_ANCHOR, ANCHOR_CENTER); ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIME, this.Time()); ::ObjectSetDouble(this.m_chart_id, this.m_name, OBJPROP_PRICE, this.Price()); ::ObjectSetString(this.m_chart_id, this.m_name, OBJPROP_TOOLTIP, this.Tooltip()); //--- Hide the created object from the chart and draw its appearance on it this.HideArrow(); this.DrawLabelView(); return true; }
创建一个图形资源,并将其锚点设置为中心,同时设置交易价格、交易时间以及提示条文本。接下来,为创建的对象设置一个标志,表明它在所有图表周期上都是隐藏的,并在其上绘制其外观。现在,为了显示它,我们只需要在所有时间框架上设置该对象的可见性标志。
绘制标签对象外观的方法:
//+------------------------------------------------------------------+ //| Draw the appearance of the label object | //+------------------------------------------------------------------+ void CDeal::DrawLabelView(void) { this.m_canvas.Erase(0x00FFFFFF); this.DrawArrow(); this.m_canvas.Update(true); }
首先,Bitmap图形对象被填充为一种完全透明的颜色,然后在其上绘制一个与交易类型相对应的箭头,接着在图表重绘时更新画布。
绘制与交易类型相对应的箭头的虚拟方法:
//+------------------------------------------------------------------+ //| Draw an arrow corresponding to the deal type | //+------------------------------------------------------------------+ void CDeal::DrawArrow(void) { switch(this.TypeDeal()) { case DEAL_TYPE_BUY : this.DrawArrowBuy(5, 10); break; case DEAL_TYPE_SELL : this.DrawArrowSell(5, 0); break; default : break; } }
根据交易类型(买入或卖出),会调用绘制相应箭头的方法。这个方法是虚拟的,因此可以在继承类中重新定义。例如,在子类中,我们可以为不同类型的交易绘制不同的标签,或者还可以额外考虑改变位置的方法等。
在画布上绘制买入箭头图标的方法:
//+------------------------------------------------------------------+ //| Draw Buy arrow mask on the canvas | //+------------------------------------------------------------------+ void CDeal::DrawArrowMaskBuy(const int shift_x, const int shift_y) { int x[]={4+shift_x, 8+shift_x, 8+shift_x, 6+shift_x, 6+shift_x, 2+shift_x, 2+shift_x, 0+shift_x, 0+shift_x, 4+shift_x}; int y[]={0+shift_y, 4+shift_y, 5+shift_y, 5+shift_y, 7+shift_y, 7+shift_y, 5+shift_y, 5+shift_y, 4+shift_y, 0+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(clrWhite, 220)); }
作为交易标签绘制的箭头在其周围有一圈白色轮廓(背景),以便在蜡烛图的深色背景上突显出来:
由于画布上绘制的图形的坐标总是相对于画布的本地坐标指定的,因此需要在折线点的X轴和Y轴坐标上添加偏移量,以便我们可以准确地将绘制的图形居中放置在画布内。接下来,考虑到传递给方法的偏移量,我们在数组中设置X和Y坐标的值,并调用Polygon()方法使用坐标点绘制白色轮廓。接下来,将使用箭头绘制方法在轮廓内绘制箭头。
在画布上绘制买入箭头的方法:
//+------------------------------------------------------------------+ //| Draw Buy arrow on the canvas | //+------------------------------------------------------------------+ void CDeal::DrawArrowBuy(const int shift_x, const int shift_y) { this.DrawArrowMaskBuy(shift_x, shift_y); int x[]={4+shift_x, 7+shift_x, 5+shift_x, 5+shift_x, 3+shift_x, 3+shift_x, 1+shift_x, 4+shift_x}; int y[]={1+shift_y, 4+shift_y, 4+shift_y, 6+shift_y, 6+shift_y, 4+shift_y, 4+shift_y, 1+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(this.m_color_arrow)); this.m_canvas.Fill(4+shift_x, 4+shift_y, ::ColorToARGB(this.m_color_arrow)); }
在这里,我们首先绘制买入箭头的背景,然后使用指定的坐标绘制买入箭头,并用颜色填充其内部空间。
绘制卖出箭头的方法与此类似:
//+------------------------------------------------------------------+ //| Draw Sell arrow mask on the canvas | //+------------------------------------------------------------------+ void CDeal::DrawArrowMaskSell(const int shift_x, const int shift_y) { int x[]={4+shift_x, 0+shift_x, 0+shift_x, 2+shift_x, 2+shift_x, 6+shift_x, 6+shift_x, 8+shift_x, 8+shift_x, 4+shift_x}; int y[]={8+shift_y, 4+shift_y, 3+shift_y, 3+shift_y, 1+shift_y, 1+shift_y, 3+shift_y, 3+shift_y, 4+shift_y, 8+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(clrWhite, 220)); } //+------------------------------------------------------------------+ //| Draw Sell arrow on the canvas | //+------------------------------------------------------------------+ void CDeal::DrawArrowSell(const int shift_x, const int shift_y) { this.DrawArrowMaskSell(shift_x, shift_y); int x[]={4+shift_x, 1+shift_x, 3+shift_x, 3+shift_x, 5+shift_x, 5+shift_x, 7+shift_x, 4+shift_x}; int y[]={7+shift_y, 4+shift_y, 4+shift_y, 2+shift_y, 2+shift_y, 4+shift_y, 4+shift_y, 7+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(this.m_color_arrow)); this.m_canvas.Fill(4+shift_x, 4+shift_y, ::ColorToARGB(this.m_color_arrow)); }
图表上隐藏和显示交易标签的方法:
//+------------------------------------------------------------------+ //| Hide the deal label on the chart | //+------------------------------------------------------------------+ void CDeal::HideArrow(const bool chart_redraw=false) { ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); } //+------------------------------------------------------------------+ //| Display the deal label on the chart | //+------------------------------------------------------------------+ void CDeal::ShowArrow(const bool chart_redraw=false) { ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
要在图表上隐藏一个对象,需要将图形对象的OBJPROP_TIMEFRAMES可见性属性设置为OBJ_NO_PERIODS。
相应地,为了显示该对象,我们需要将OBJPROP_TIMEFRAMES属性设置为OBJ_ALL_PERIODS。
设置交易标签颜色的方法:
//+------------------------------------------------------------------+ //| Set the deal label color | //+------------------------------------------------------------------+ void CDeal::SetColorArrow(const color clr) { this.m_color_arrow=clr; this.DrawLabelView(); }
m_color_arrow变量存储着绘制标签的颜色,其值被设置为传递给该方法的值,然后整个图形对象会完全重新绘制。因此,可以从控制程序中“即时”更改图表上交易标签的颜色。
我们创建了一个交易类,它可以通过订单号访问历史交易的属性,并允许我们获取该交易的必要数据,以及在图表上显示或隐藏其标签。
类对象将存储在持仓对象交易的列表中,该列表进而会显示历史持仓的属性,提供对其交易和参数的访问,并显示通过线条连接的开仓和平仓交易。
持仓类
在与交易类相同的文件夹中,创建一个名为Position.mqh的新文件,用于定义CPosition类。
与交易类类似,实现对象属性的枚举,以便按持仓属性进行搜索和排序,并为持仓类连接交易类文件和CArrayObj:
//+------------------------------------------------------------------+ //| Position.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "Deal.mqh" #include <Arrays\ArrayObj.mqh> enum ENUM_POSITION_SORT_MODE { SORT_MODE_POSITION_TICKET = 0, // Mode of comparing/sorting by a position ticket SORT_MODE_POSITION_TIME, // Mode of comparing/sorting by position open time SORT_MODE_POSITION_TIME_MSC, // Mode of comparing/sorting by position open time im milliseconds SORT_MODE_POSITION_TIME_UPDATE, // Mode of comparing/sorting by position update time SORT_MODE_POSITION_TIME_UPDATE_MSC, // Mode of comparing/sorting by position update time im milliseconds SORT_MODE_POSITION_TYPE, // Mode of comparing/sorting by position type SORT_MODE_POSITION_MAGIC, // Mode of comparing/sorting by a position magic number SORT_MODE_POSITION_IDENTIFIER, // Mode of comparing/sorting by a position ID SORT_MODE_POSITION_REASON, // Mode of comparing/sorting by position open reason SORT_MODE_POSITION_VOLUME, // Mode of comparing/sorting by a position volume SORT_MODE_POSITION_PRICE_OPEN, // Mode of comparing/sorting by a position price SORT_MODE_POSITION_SL, // Mode of comparing/sorting by Stop Loss level for an open position SORT_MODE_POSITION_TP, // Mode of comparing/sorting by Take Profit level for an open position SORT_MODE_POSITION_PRICE_CURRENT, // Mode of comparing/sorting by the current symbol price SORT_MODE_POSITION_SWAP, // Mode of comparing/sorting by accumulated swap SORT_MODE_POSITION_PROFIT, // Mode of comparing/sorting by the current profit SORT_MODE_POSITION_SYMBOL, // Mode of comparing/sorting by a symbol a position is opened on SORT_MODE_POSITION_COMMENT, // Mode of comparing/sorting by a position comment SORT_MODE_POSITION_EXTERNAL_ID, // Mode of comparing/sorting by a position ID in an external system SORT_MODE_POSITION_TIME_CLOSE, // Mode of comparing/sorting by a position open time SORT_MODE_POSITION_TIME_CLOSE_MSC, // Mode of comparing/sorting by a position open time in milliseconds SORT_MODE_POSITION_PRICE_CLOSE, // Mode of comparing/sorting by a position price }; //+------------------------------------------------------------------+ //| Position class | //+------------------------------------------------------------------+ class CPosition : public CObject { }
在类的受保护和公共部分中,输入处理类所需的变量和方法:
//+------------------------------------------------------------------+ //| Position class | //+------------------------------------------------------------------+ class CPosition : public CObject { private: protected: CArrayObj m_list_deals; // List of position deals CDeal m_temp_deal; // Temporary deal object for searching by property in the list //--- Return time with milliseconds string TimeMscToString(const long time_msc,int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const; //--- Integer properties long m_ticket; // Position ticket datetime m_time; // Position opening time long m_time_msc; // Position opening time in milliseconds since 01.01.1970 datetime m_time_update; // Position update time long m_time_update_msc; // Position update time in milliseconds since 01.01.1970 ENUM_POSITION_TYPE m_type; // Position type long m_magic; // Magic number for a position (see ORDER_MAGIC) long m_identifier; // Position ID ENUM_POSITION_REASON m_reason; // Position opening reason //--- Real properties double m_volume; // Position volume double m_price_open; // Position price double m_sl; // Stop Loss level for an open position double m_tp; // Take Profit level for an open position double m_price_current; // Current price by symbol double m_swap; // Accumulated swap double m_profit; // Current profit //--- String properties string m_symbol; // A symbol the position is open for string m_comment; // Position comment string m_external_id; // Position ID in an external system (on the exchange) //--- Additional properties long m_chart_id; // Chart ID int m_profit_pt; // Profit in points int m_digits; // Symbol digits double m_point; // One symbol point value double m_contract_size; // Symbol trade contract size string m_currency_profit; // Symbol profit currency string m_account_currency; // Deposit currency string m_line_name; // Line graphical object name color m_line_color; // Connecting line color //--- Create a line connecting open-close deals virtual bool CreateLine(void); //--- Return the pointer to (1) open and (2) close deal CDeal *GetDealIn(void) const; CDeal *GetDealOut(void) const; //--- (1) Hide and (2) display deal labels on the chart void HideDeals(const bool chart_redraw=false); void ShowDeals(const bool chart_redraw=false); //--- (1) Hide and (2) display the connecting line between the deal labels void HideLine(const bool chart_redraw=false); void ShowLine(const bool chart_redraw=false); public: //--- Set the properties //--- Integer properties void SetTicket(const long ticket) { this.m_ticket=ticket; } // Position ticket void SetTime(const datetime time) { this.m_time=time; } // Position open time void SetTimeMsc(const long value) { this.m_time_msc=value; } // Position open time in milliseconds 01.01.1970 void SetTimeUpdate(const datetime time) { this.m_time_update=time; } // Position update time void SetTimeUpdateMsc(const long value) { this.m_time_update_msc=value; } // Position update time in milliseconds 01.01.1970 void SetTypePosition(const ENUM_POSITION_TYPE type) { this.m_type=type; } // Position type void SetMagic(const long magic) { this.m_magic=magic; } // Magic number for a position (see ORDER_MAGIC) void SetID(const long id) { this.m_identifier=id; } // Position ID void SetReason(const ENUM_POSITION_REASON reason) { this.m_reason=reason; } // Position opening reason //--- Real properties void SetVolume(const double volume) { this.m_volume=volume; } // Position volume void SetPriceOpen(const double price) { this.m_price_open=price; } // Position price void SetSL(const double value) { this.m_sl=value; } // Stop Loss level for an open position void SetTP(const double value) { this.m_tp=value; } // Take Profit level for an open position void SetPriceCurrent(const double price) { this.m_price_current=price; } // Current price by symbol void SetSwap(const double value) { this.m_swap=value; } // Accumulated swap void SetProfit(const double value) { this.m_profit=value; } // Current profit //--- String properties void SetSymbol(const string symbol) { this.m_symbol=symbol; } // Symbol a position is opened for void SetComment(const string comment) { this.m_comment=comment; } // Position comment void SetExternalID(const string ext_id) { this.m_external_id=ext_id; } // Position ID in an external system (on the exchange) //--- Get the properties //--- Integer properties long Ticket(void) const { return(this.m_ticket); } // Position ticket datetime Time(void) const { return(this.m_time); } // Position open time long TimeMsc(void) const { return(this.m_time_msc); } // Position open time in milliseconds since 01.01.1970 datetime TimeUpdate(void) const { return(this.m_time_update); } // Position update time long TimeUpdateMsc(void) const { return(this.m_time_update_msc);} // Position update time in milliseconds since 01.01.1970 ENUM_POSITION_TYPE TypePosition(void) const { return(this.m_type); } // Position type long Magic(void) const { return(this.m_magic); } // Magic number for a position (see ORDER_MAGIC) long ID(void) const { return(this.m_identifier); } // Position ID ENUM_POSITION_REASON Reason(void) const { return(this.m_reason); } // Position opening reason //--- Real properties double Volume(void) const { return(this.m_volume); } // Position volume double PriceOpen(void) const { return(this.m_price_open); } // Position price double SL(void) const { return(this.m_sl); } // Stop Loss level for an open position double TP(void) const { return(this.m_tp); } // Take Profit for an open position double PriceCurrent(void) const { return(this.m_price_current); } // Current price by symbol double Swap(void) const { return(this.m_swap); } // Accumulated swap double Profit(void) const { return(this.m_profit); } // Current profit //--- String properties string Symbol(void) const { return(this.m_symbol); } // A symbol position is opened on string Comment(void) const { return(this.m_comment); } // Position comment string ExternalID(void) const { return(this.m_external_id); } // Position ID in an external system (on the exchange) //--- Additional properties ulong DealIn(void) const; // Open deal ticket ulong DealOut(void) const; // Close deal ticket datetime TimeClose(void) const; // Close time long TimeCloseMsc(void) const; // Close time in milliseconds int ProfitInPoints(void) const; // Profit in points double PriceClose(void) const; // Close price //--- Add a deal to the list of deals, return the pointer CDeal *DealAdd(const long ticket); //--- Set the color of the (1) connecting line, (2) Buy and Sell deals void SetLineColor(const color clr=C'225,68,29'); void SetDealsColor(const color clr_deal_buy=C'3,95,172', const color clr_deal_sell=C'225,68,29'); //--- Return a position type description string TypeDescription(void) const; //--- Return position open time and price description string TimePriceCloseDescription(void); //--- Return position close time and price description string TimePriceOpenDescription(void); //--- Return a brief position description string Description(void); //--- Returns a text of a position popup message virtual string Tooltip(void); //--- Print the properties of the position and its deals in the journal void Print(void); //--- (1) Hide and (2) display a graphical representation of a position on a chart void Hide(const bool chart_redraw=false); void Show(const bool chart_redraw=false); //--- Compare two objects by the property specified in 'mode' virtual int Compare(const CObject *node, const int mode=0) const; //--- Constructor/destructor CPosition(const long position_id, const string symbol); CPosition(void) { this.m_symbol=::Symbol(); } ~CPosition(); };
为了在类中对指向CObject类及其后代实例的动态数组指针的已排序列表进行搜索,CArrayObj类的Search()方法接收一个指向对象实例的指针,我们应该根据用于对列表进行排序的属性在该对象中进行比较。为了避免不断创建和销毁具有给定属性的新对象(这在性能方面相当昂贵),我们在类的受保护部分中声明了一个交易对象的实例。为了执行搜索,我们只需在此对象中设置所需属性的所需值,并将其作为期望的实例传递给搜索方法。
我们来仔细分析所声明的方法。
参数型构造函数:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CPosition::CPosition(const long position_id, const string symbol) { this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); this.m_identifier = position_id; this.m_account_currency = ::AccountInfoString(ACCOUNT_CURRENCY); this.m_symbol = (symbol==NULL ? ::Symbol() : symbol); this.m_digits = (int)::SymbolInfoInteger(this.m_symbol,SYMBOL_DIGITS); this.m_point = ::SymbolInfoDouble(this.m_symbol,SYMBOL_POINT); this.m_contract_size = ::SymbolInfoDouble(this.m_symbol,SYMBOL_TRADE_CONTRACT_SIZE); this.m_currency_profit = ::SymbolInfoString(this.m_symbol,SYMBOL_CURRENCY_PROFIT); this.m_chart_id = ::ChartID(); this.m_line_name = "line#"+(string)this.m_identifier; this.m_line_color = C'225,68,29'; }
构造函数接收持仓ID和持仓开仓的交易品种。持仓交易列表接收按交易时间(毫秒)排序的标志。ID和交易品种被保存在类变量中,同时设置了一些账户和交易品种的参数,以及图表ID和连接持仓开仓和平仓交易标签的线条的属性。
在类的析构函数中,移除所有前缀为线条名称的图形对象,并清空交易列表:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CPosition::~CPosition() { ::ObjectDelete(this.m_chart_id, this.m_line_name); this.m_list_deals.Clear(); }
通过前缀来删除图形线条对象,原因如下:如果继承类要显示连接所有持仓交易(而不仅仅是开仓和平仓交易)的几条线,那么每条线的名称可以设置为“line_name”+“line_number”。在这种情况下,所有这些线条仍然会在析构函数中被删除,因为它们都有一个共同的前缀——“line name”。
通过指定属性比较两个对象的方法:
//+------------------------------------------------------------------+ //| Compare two objects by the specified property | //+------------------------------------------------------------------+ int CPosition::Compare(const CObject *node,const int mode=0) const { const CPosition *obj=node; switch(mode) { case SORT_MODE_POSITION_TICKET : return(this.Ticket() > obj.Ticket() ? 1 : this.Ticket() < obj.Ticket() ? -1 : 0); case SORT_MODE_POSITION_TIME : return(this.Time() > obj.Time() ? 1 : this.Time() < obj.Time() ? -1 : 0); case SORT_MODE_POSITION_TIME_MSC : return(this.TimeMsc() > obj.TimeMsc() ? 1 : this.TimeMsc() < obj.TimeMsc() ? -1 : 0); case SORT_MODE_POSITION_TIME_UPDATE : return(this.TimeUpdate() > obj.TimeUpdate() ? 1 : this.TimeUpdate() < obj.TimeUpdate() ? -1 : 0); case SORT_MODE_POSITION_TIME_UPDATE_MSC: return(this.TimeUpdateMsc() > obj.TimeUpdateMsc() ? 1 : this.TimeUpdateMsc() < obj.TimeUpdateMsc() ? -1 : 0); case SORT_MODE_POSITION_TYPE : return(this.TypePosition() > obj.TypePosition() ? 1 : this.TypePosition() < obj.TypePosition() ? -1 : 0); case SORT_MODE_POSITION_MAGIC : return(this.Magic() > obj.Magic() ? 1 : this.Magic() < obj.Magic() ? -1 : 0); case SORT_MODE_POSITION_IDENTIFIER : return(this.ID() > obj.ID() ? 1 : this.ID() < obj.ID() ? -1 : 0); case SORT_MODE_POSITION_REASON : return(this.Reason() > obj.Reason() ? 1 : this.Reason() < obj.Reason() ? -1 : 0); case SORT_MODE_POSITION_VOLUME : return(this.Volume() > obj.Volume() ? 1 : this.Volume() < obj.Volume() ? -1 : 0); case SORT_MODE_POSITION_PRICE_OPEN : return(this.PriceOpen() > obj.PriceOpen() ? 1 : this.PriceOpen() < obj.PriceOpen() ? -1 : 0); case SORT_MODE_POSITION_SL : return(this.SL() > obj.SL() ? 1 : this.SL() < obj.SL() ? -1 : 0); case SORT_MODE_POSITION_TP : return(this.TP() > obj.TP() ? 1 : this.TP() < obj.TP() ? -1 : 0); case SORT_MODE_POSITION_PRICE_CURRENT : return(this.PriceCurrent() > obj.PriceCurrent() ? 1 : this.PriceCurrent() < obj.PriceCurrent() ? -1 : 0); case SORT_MODE_POSITION_SWAP : return(this.Swap() > obj.Swap() ? 1 : this.Swap() < obj.Swap() ? -1 : 0); case SORT_MODE_POSITION_PROFIT : return(this.Profit() > obj.Profit() ? 1 : this.Profit() < obj.Profit() ? -1 : 0); case SORT_MODE_POSITION_SYMBOL : return(this.Symbol() > obj.Symbol() ? 1 : this.Symbol() < obj.Symbol() ? -1 : 0); case SORT_MODE_POSITION_COMMENT : return(this.Comment() > obj.Comment() ? 1 : this.Comment() < obj.Comment() ? -1 : 0); case SORT_MODE_POSITION_EXTERNAL_ID : return(this.ExternalID() > obj.ExternalID() ? 1 : this.ExternalID() < obj.ExternalID() ? -1 : 0); case SORT_MODE_POSITION_TIME_CLOSE : return(this.TimeClose() > obj.TimeClose() ? 1 : this.TimeClose() < obj.TimeClose() ? -1 : 0); case SORT_MODE_POSITION_TIME_CLOSE_MSC : return(this.TimeCloseMsc() > obj.TimeCloseMsc() ? 1 : this.TimeCloseMsc() < obj.TimeCloseMsc() ? -1 : 0); case SORT_MODE_POSITION_PRICE_CLOSE : return(this.PriceClose() > obj.PriceClose() ? 1 : this.PriceClose() < obj.PriceClose() ? -1 : 0); default : return -1; } }
在开发交易对象类时也考虑了类似的方法。这里的逻辑是相似的:如果当前对象的属性值大于正在比较的属性值,则返回1;如果小于,则返回-1;如果属性相等,则返回0。
返回带毫秒的时间的方法:
//+------------------------------------------------------------------+ //| Return time with milliseconds | //+------------------------------------------------------------------+ string CPosition::TimeMscToString(const long time_msc, int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const { return(::TimeToString(time_msc/1000, flags) + "." + ::IntegerToString(time_msc %1000, 3, '0')); }
这是交易类中同交易对象类同名方法的副本。
返回开仓交易指针的方法:
//+------------------------------------------------------------------+ //| Return the pointer to the opening deal | //+------------------------------------------------------------------+ CDeal *CPosition::GetDealIn(void) const { int total=this.m_list_deals.Total(); for(int i=0; i<total; i++) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; if(deal.Entry()==DEAL_ENTRY_IN) return deal; } return NULL; }
我们需要在交易对象列表中,通过持仓更新方法找到一条开仓交易记录。这条交易在时间排序的列表中应该是第一条。因此,我们从列表的开头——即索引0处开始循环。
我们通过索引从列表中获取一条交易,如果这是一条市场开仓交易,则返回在列表中找到的该交易的指针。循环结束时,如果未找到交易记录,则返回NULL。
返回平仓交易指针的方法:
//+------------------------------------------------------------------+ //| Return the pointer to the close deal | //+------------------------------------------------------------------+ CDeal *CPosition::GetDealOut(void) const { for(int i=this.m_list_deals.Total()-1; i>=0; i--) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) return deal; } return NULL; }
与前面的方法相比,这里的一切都相反——平仓交易位于列表的最末尾,因此我们从列表的末尾开始循环。
一旦找到平仓交易,就返回其指针。否则,返回NULL。
获取历史持仓其他属性的方法:
//+------------------------------------------------------------------+ //| Return the open deal ticket | //+------------------------------------------------------------------+ ulong CPosition::DealIn(void) const { CDeal *deal=this.GetDealIn(); return(deal!=NULL ? deal.Ticket() : 0); } //+------------------------------------------------------------------+ //| Return the close deal ticket | //+------------------------------------------------------------------+ ulong CPosition::DealOut(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.Ticket() : 0); } //+------------------------------------------------------------------+ //| Return the close time | //+------------------------------------------------------------------+ datetime CPosition::TimeClose(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.Time() : 0); } //+------------------------------------------------------------------+ //| Return the close time in milliseconds | //+------------------------------------------------------------------+ long CPosition::TimeCloseMsc(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.TimeMsc() : 0); } //+------------------------------------------------------------------+ //| Return the close price | //+------------------------------------------------------------------+ double CPosition::PriceClose(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.Price() : 0); }
在所有方法中,我们首先获取交易的指针,从该指针中获取相应的属性,如果指针有效,则返回属性值,否则返回零。
返回点数利润的方法:
//+------------------------------------------------------------------+ //| Return a profit in points | //+------------------------------------------------------------------+ int CPosition::ProfitInPoints(void) const { //--- If symbol Point has not been received previously, inform of that and return 0 if(this.m_point==0) { ::Print("The Point() value could not be retrieved."); return 0; } //--- Get position open and close prices double open =this.PriceOpen(); double close=this.PriceClose(); //--- If failed to get the prices, return 0 if(open==0 || close==0) return 0; //--- Depending on the position type, return the calculated value of the position profit in points return int(this.TypePosition()==POSITION_TYPE_BUY ? (close-open)/this.m_point : (open-close)/this.m_point); }
在这里,我们首先从相应的交易中获取开仓和平仓价格,然后根据持仓方向返回点数利润的计算结果。
将交易添加到交易列表中的方法:
//+------------------------------------------------------------------+ //| Add a deal to the list of deals | //+------------------------------------------------------------------+ CDeal *CPosition::DealAdd(const long ticket) { //--- A temporary object gets a ticket of the desired deal and the flag of sorting the list of deals by ticket this.m_temp_deal.SetTicket(ticket); this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET); //--- Set the result of checking if a deal with such a ticket is present in the list bool added=(this.m_list_deals.Search(&this.m_temp_deal)!=WRONG_VALUE); //--- Set the flag of sorting by time in milliseconds for the list this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); //--- If a deal with such a ticket is already in the list, return NULL if(added) return NULL; //--- Create a new deal object CDeal *deal=new CDeal(ticket); if(deal==NULL) return NULL; //--- Add the created object to the list in sorting order by time in milliseconds //--- If failed to add the deal to the list, remove the the deal object and return NULL if(!this.m_list_deals.InsertSort(deal)) { delete deal; return NULL; } //--- If this is a position closing deal, set the profit from the deal properties to the position profit value and //--- create a connecting line between the position open-close deal labels if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) { this.SetProfit(deal.Profit()); this.CreateLine(); } //--- Return the pointer to the created deal object return deal; }
该方法接收选定交易的编号。首先,确保列表中不存在具有该编号的交易。为此,将临时交易对象的编号设置为传递给方法的编号。交易列表按交易编号的值进行排序,并在列表中搜索具有该编号的交易。搜索交易后,立即将列表的排序标志恢复为按毫秒时间排序。如果列表中已经存在该交易,则无需添加,方法返回NULL。同时,交易列表的排序已经默认设置为按毫秒时间排序。如果列表中不存在该交易,则创建一个新的交易对象,并按毫秒时间排序的顺序将其添加到列表中。如果这是一个平仓交易,则将其利润设置在持仓属性中,并在开仓交易和平仓交易之间创建一条连接线。最后,返回新创建的交易对象的指针。
返回持仓类型描述的方法:
//+------------------------------------------------------------------+ //| Return a position type description | //+------------------------------------------------------------------+ string CPosition::TypeDescription(void) const { return(this.m_type==POSITION_TYPE_BUY ? "Buy" : this.m_type==POSITION_TYPE_SELL ? "Sell" : "Unknown::"+(string)this.m_type); }
根据存储在变量m_type中的头寸类型返回描述。如果变量值不匹配头寸类型枚举常量,则返回 "Unknown::" + 变量值。
返回头寸平仓时间及价格描述的方法:
//+------------------------------------------------------------------+ //| Return position close time and price description | //+------------------------------------------------------------------+ string CPosition::TimePriceCloseDescription(void) { if(this.TimeCloseMsc()==0) return "Not closed yet"; return(::StringFormat("Closed %s [%.*f]", this.TimeMscToString(this.TimeCloseMsc()),this.m_digits, this.PriceClose())); }
如果头寸对象中还没有成交的平仓交易,该方法将返回一条消息,表明该头寸尚未平仓。否则,将创建并返回以下类型的字符串:
已平仓 2023.06.12 17:04:20.362 [1.07590]
该方法返回头寸的开仓时间和价格的描述:
//+------------------------------------------------------------------+ //| Return position open time and price description | //+------------------------------------------------------------------+ string CPosition::TimePriceOpenDescription(void) { return(::StringFormat("Opened %s [%.*f]", this.TimeMscToString(this.TimeMsc()),this.m_digits, this.PriceOpen())); }
如下形式的字符串将被创建并返回:
Opened 2023.06.12 16:51:36.838 [1.07440]
这个方法返回一个关于头寸的简要描述:
//+------------------------------------------------------------------+ //| Return a brief position description | //+------------------------------------------------------------------+ string CPosition::Description(void) { return(::StringFormat("Position %s %.2f %s #%I64d, Magic %I64d", this.Symbol(), this.Volume(), this.TypeDescription(), this.ID(), this.Magic())); }
返回一个如下形式的字符串:
Position EURUSD 0.10 Buy #1752955040, Magic 123
该虚拟方法返回头寸弹出消息的文本:
//+------------------------------------------------------------------+ //| Return a text of a position pop-up message | //+------------------------------------------------------------------+ string CPosition::Tooltip(void) { //--- Get the pointers to the open and close deals CDeal *deal_in =this.GetDealIn(); CDeal *deal_out=this.GetDealOut(); //--- If no deals are received, return an empty string if(deal_in==NULL || deal_out==NULL) return NULL; //--- Get commission, swap and deal fee that are common for two deals double commission=deal_in.Commission()+deal_out.Commission(); double swap=deal_in.Swap()+deal_out.Swap(); double fee=deal_in.Fee()+deal_out.Fee(); //--- Get the final profit of the position and the spread values when opening and closing double profit=deal_out.Profit(); int spread_in=deal_in.Spread(); int spread_out=deal_out.Spread(); //--- If the reason for closing the position is StopLoss, TakeProfit or StopOut, set the reason description in the variable string reason=(deal_out.Reason()==DEAL_REASON_SL || deal_out.Reason()==DEAL_REASON_TP || deal_out.Reason()==DEAL_REASON_SO ? deal_out.ReasonDescription() : ""); //--- Create and return the tooltip string return(::StringFormat("%s\nCommission %.2f, Swap %.2f, Fee %.2f\nSpread In/Out %d/%d, Profit %+.2f %s (%d points)\nResult: %s %+.2f %s", this.Description(), commission, swap, fee, spread_in, spread_out, profit,this.m_currency_profit, this.ProfitInPoints(), reason, profit+commission+fee+swap, this.m_currency_profit)); }
方法创建并返回如下形式的字符串:
Position EURUSD 0.10 Buy #1752955040, Magic 0 Commission 0.00, Swap 0.00, Fee 0.00 Spread In/Out 0/0, Profit +15.00 USD (150 points) Result: TP +15.00 USD
该文本被设置为连接仓位开仓和平仓交易的字符串的提示条文本。该方法是虚拟的,如果需要输出略有不同的数据,可以在继承的类中重写。当使用该方法返回的字符串作为提示条文本时,我们应该考虑到字符串的长度,因为提示条的字符串长度有限制,大约为160个字符(包括控制码)。不幸的是,我无法给出确切的值,因为这是通过经验得出的。
在图表上显示交易标签的方法:
//+------------------------------------------------------------------+ //| Display deal labels on the chart | //+------------------------------------------------------------------+ void CPosition::ShowDeals(const bool chart_redraw=false) { for(int i=this.m_list_deals.Total()-1; i>=0; i--) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; deal.ShowArrow(); } if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
在交易列表的循环中,获取每个连续交易并调用接收对象的 ShowArrow() 方法,该方法在图表上显示交易标签。在循环结束时,如果设置了相应的标志,则重新绘制图表。
隐藏图表上交易标签的方法:
//+------------------------------------------------------------------+ //| Hide deal labels on the chart | //+------------------------------------------------------------------+ void CPosition::HideDeals(const bool chart_redraw=false) { for(int i=this.m_list_deals.Total()-1; i>=0; i--) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; deal.HideArrow(); } if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
在交易列表的循环中,我们获取每笔连续交易并调用接收对象的 HideArrow() 方法,该方法会隐藏图表中的交易标签。在循环结束时,如果设置了相应的标志,则重新绘制图表。
创建开仓和平仓连接线的方法:
//+------------------------------------------------------------------+ //| Create a line connecting open-close deals | //+------------------------------------------------------------------+ bool CPosition::CreateLine(void) { //--- If the graphical line object could not be created, report this in the journal and return 'false' ::ResetLastError(); if(!::ObjectCreate(this.m_chart_id, this.m_line_name, OBJ_TREND, 0, 0, 0, 0, 0)) { ::Print("ObjectCreate() failed. Error ", ::GetLastError()); return false; } //--- Hide the line this.HideLine(); //--- Set the line to be drawn with dots, define the color and return 'true' ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_STYLE, STYLE_DOT); ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_COLOR, this.m_line_color); return true; }
在原点位置 (价格为 0,时间为 01.01.1970 00:00:00) 创建一条具有价格和时间坐标的线。从图表中隐藏该线,并将绘图样式设置为点和默认颜色。
最初,每个对象的线都是隐藏的。当需要显示它们时,为它们设置必要的坐标和显示模式。
显示交易标签之间的连接线的方法:
//+------------------------------------------------------------------+ //| Display the connecting line between deal labels | //+------------------------------------------------------------------+ void CPosition::ShowLine(const bool chart_redraw=false) { //--- Get the pointers to the open and close deals CDeal *deal_in= this.GetDealIn(); CDeal *deal_out=this.GetDealOut(); //--- If no deals are received, leave if(deal_in==NULL || deal_out==NULL) return; //--- Set a start and end time, a price from the deal properties and a tooltip text for the line ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIME, 0, deal_in.Time()); ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIME, 1, deal_out.Time()); ::ObjectSetDouble(this.m_chart_id, this.m_line_name, OBJPROP_PRICE, 0, deal_in.Price()); ::ObjectSetDouble(this.m_chart_id, this.m_line_name, OBJPROP_PRICE, 1, deal_out.Price()); ::ObjectSetString(this.m_chart_id, this.m_line_name, OBJPROP_TOOLTIP, this.Tooltip()); //--- Show the line on the chart and update it if the flag is set ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
隐藏交易标签之间的连接线的方法:
//+------------------------------------------------------------------+ //| Hide the connecting line between the deal labels | //+------------------------------------------------------------------+ void CPosition::HideLine(const bool chart_redraw=false) { ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
该对象的可见性标志在所有图表周期上都会被重置,如果设置了该标志,则图表会进行更新。
设置连接线颜色的方法:
//+------------------------------------------------------------------+ //| Set the color of the connecting line | //+------------------------------------------------------------------+ void CPosition::SetLineColor(const color clr=C'225,68,29') { if(::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_COLOR, clr)) this.m_line_color=clr; }
传递给该方法的值将作为对象的 color 属性进行设置。如果一切成功,则将颜色设置为m_line_color变量。
设置买卖交易颜色的方法:
//+------------------------------------------------------------------+ //| Set Buy and Sell deal color | //+------------------------------------------------------------------+ void CPosition::SetDealsColor(const color clr_deal_buy=C'3,95,172', const color clr_deal_sell=C'225,68,29') { //--- In the loop by the list of deals int total=this.m_list_deals.Total(); for(int i=0; i<total; i++) { //--- get the next deal object CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; //--- In case of Buy deal type, set a color for a Buy deal for the object if(deal.TypeDeal()==DEAL_TYPE_BUY) deal.SetColorArrow(clr_deal_buy); //--- In case of Sell deal type, set a color for a Sell deal for the object if(deal.TypeDeal()==DEAL_TYPE_SELL) deal.SetColorArrow(clr_deal_sell); } }
该方法接收买入和卖出交易颜色。在循环遍历持仓交易列表时,获取指向每个连续交易的指针,并根据交易类型设置交易标签的颜色。
在图表上图形化表示仓位的方法:
//+------------------------------------------------------------------+ //| Display a graphical representation of a position on a chart | //+------------------------------------------------------------------+ void CPosition::Show(const bool chart_redraw=false) { this.ShowDeals(false); this.ShowLine(chart_redraw); }
该方法依次调用受保护的方法,这些方法会在图表上显示入场交易、出场交易以及连接它们的线条。
在图表上隐藏仓位图形的方法:
//+------------------------------------------------------------------+ //| Hide a graphical representation of a position on a chart | //+------------------------------------------------------------------+ void CPosition::Hide(const bool chart_redraw=false) { this.HideLine(false); this.HideDeals(chart_redraw); }
该方法依次调用受保护的方法,这些方法会隐藏图表上的入场交易、出场交易以及连接它们的线条。
上面提到的两个方法是公开的,用于从控制程序中控制仓位在图表上的显示。
在日志中打印仓位属性和交易的方法:
//+------------------------------------------------------------------+ //| Print the position properties and deals in the journal | //+------------------------------------------------------------------+ void CPosition::Print(void) { ::PrintFormat("%s\n-%s\n-%s", this.Description(), this.TimePriceOpenDescription(), this.TimePriceCloseDescription()); for(int i=0; i<this.m_list_deals.Total(); i++) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; deal.Print(); } }
首先,会显示一个标题,其中包含仓位的简要描述,以及两行内容,分别显示仓位开仓和平仓的时间及价格。接下来,会循环打印列表中所有仓位交易的描述。
因此,我们在日志中得到以下数据:
Position EURUSD 0.10 Sell #2523224572, Magic 0 -Opened 2024.05.31 17:06:15.134 [1.08734] -Closed 2024.05.31 17:33:17.772 [1.08639] Deal: Entry In 0.10 Sell #2497852906 at 2024.05.31 17:06:15.134 Deal: Entry Out 0.10 Buy #2497993663 at 2024.05.31 17:33:17.772
这信息量不是很大,但这些类中的Print()方法仅用于调试。
现在我们已经有了两个类:交易类(deal class)和仓位类(position class),其中仓位类包含一个交易列表,这些交易参与了仓位的整个生命周期。这些类很简单,包含了交易的基本信息,以及一些关于仓位开仓/平仓时的价格和时间以及以点数计算的盈利的附加数据。剩余的方法用于获取和显示这些信息到图表或文本行上。
现在让我们创建一个通用类,该类将从交易中收集所有仓位,并将它们放置在一个历史仓位列表中。该类将提供对仓位属性及其交易的访问,更新和补充历史仓位列表,并在图表上显示指定的仓位。
历史仓单管理类
在之前创建两个类的同一个文件夹中,新建一个名为PositionsControl.mqh的文件,用于定义CPositionsControl类。
该类应该继承自CObject标准库基对象,同时将仓位类文件包含进CPositionsControl新类文件中:
//+------------------------------------------------------------------+ //| PositionsControl.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "Position.mqh" //+------------------------------------------------------------------+ //| Class of historical positions | //+------------------------------------------------------------------+ class CPositionsControl : public CObject { }
在类的私有(private)、受保护(protected)和公共(public)部分中声明变量和方法以处理该类:
//+------------------------------------------------------------------+ //| Class of historical positions | //+------------------------------------------------------------------+ class CPositionsControl : public CObject { private: string m_symbol; // The symbol the position is open for long m_current_id; // ID of the current position displayed on the chart bool m_key_ctrl; // Flag for allowing to control the chart using the keyboard //--- Return the position type by deal type ENUM_POSITION_TYPE PositionTypeByDeal(const CDeal *deal); protected: CPosition m_temp_pos; // Temporary position object for searching CArrayObj m_list_pos; // List of positions long m_chart_id; // Chart ID //--- Return the position object from the list by ID CPosition *GetPositionObjByID(const long id); //--- Return the flag of the market position bool IsMarketPosition(const long id); //--- Return the pointer to the (1) first and the (2) last open position in the list CPosition *GetFirstClosedPosition(void); CPosition *GetLastClosedPosition(void); //--- Return the pointer to the (1) previous and (2) next closed position in the list CPosition *GetPrevClosedPosition(CPosition *current); CPosition *GetNextClosedPosition(CPosition *current); //--- Displays a graphical representation of the specified position on a chart void Show(CPosition *pos, const bool chart_redraw=false); //--- Center the chart on the currently selected position void CentersChartByCurrentSelected(void); //--- Return the ID of the current selected position long CurrentSelectedID(void) const { return this.m_current_id; } //--- Return the selected position (1) open and (2) close time datetime TimeOpenCurrentSelected(void); datetime TimeCloseCurrentSelected(void); //--- Hide the graphical representation of all positions on the chart except the specified one void HideAllExceptOne(const long pos_id, const bool chart_redraw=false); public: //--- Return the chart (1) symbol and (2) ID string Symbol(void) const { return this.m_symbol; } long ChartID(void) const { return this.m_chart_id; } //--- Create and update the list of positions. It can be redefined in the inherited classes virtual bool Refresh(void); //--- Return the number of positions in the list int Total(void) const { return this.m_list_pos.Total(); } //--- (1) Hide and (2) display the graphical representation of the first position on the chart void HideFirst(const bool chart_redraw=false); void ShowFirst(const bool chart_redraw=false); //--- (1) Hide and (2) display the graphical representation of the last position on the chart void HideLast(const bool chart_redraw=false); void ShowLast(const bool chart_redraw=false); //--- Display a graphical representation of the (1) current, (2) previous and (3) next position void ShowCurrent(const bool chart_redraw=false); void ShowPrev(const bool chart_redraw=false); void ShowNext(const bool chart_redraw=false); //--- Return the description of the currently selected position string CurrentSelectedDescription(void); //--- Print the properties of all positions and their deals in the journal void Print(void); //--- Constructor/destructor CPositionsControl(const string symbol=NULL); ~CPositionsControl(); };
我们来详细分析所声明的方法。
类的构造函数:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CPositionsControl::CPositionsControl(const string symbol=NULL) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); this.m_symbol = (symbol==NULL ? ::Symbol() : symbol); this.m_chart_id = ::ChartID(); this.m_current_id = 0; this.m_key_ctrl = ::ChartGetInteger(this.m_chart_id, CHART_KEYBOARD_CONTROL); }
为历史仓位列表设置按平仓时间的毫秒数进行排序的标志。将传递给构造函数的交易品种设置到变量中。如果该字符串为空,则表示它是程序正在运行的图表上的当前交易品种。图表ID设置为当前图表ID。在继承类中可能会设置不同的ID。在m_key_ctrl变量中,设置允许使用键盘控制图表的标志。程序运行后,该值将恢复为图表属性,以便将图表恢复到程序启动前的状态。
在类的析构函数中,仓位列表会被销毁,同时CHART_KEYBOARD_CONTROL图表属性会恢复为程序启动前的值:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CPositionsControl::~CPositionsControl() { this.m_list_pos.Shutdown(); ::ChartSetInteger(this.m_chart_id, CHART_KEYBOARD_CONTROL, this.m_key_ctrl); }
通过ID从列表中返回仓位对象的方法:
//+------------------------------------------------------------------+ //| Return the position object from the list by ID | //+------------------------------------------------------------------+ CPosition *CPositionsControl::GetPositionObjByID(const long id) { //--- Set the position ID for the temporary object and set the flag of sorting by position ID for the list this.m_temp_pos.SetID(id); this.m_list_pos.Sort(SORT_MODE_POSITION_IDENTIFIER); //--- Get the index of the position object with such an ID (or -1 if it is absent) from the list //--- Use the obtained index to get the pointer to the position object from the list (or NULL if the index value is -1) int index=this.m_list_pos.Search(&this.m_temp_pos); CPosition *pos=this.m_list_pos.At(index); //--- Set the flag of sorting by position close time in milliseconds for the list and //--- return the pointer to the position object (or NULL if it is absent) this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); return pos; }
代码注释中完整描述了方法逻辑。
返回市场仓位标志的方法:
//+------------------------------------------------------------------+ //| Return the market position flag | //+------------------------------------------------------------------+ bool CPositionsControl::IsMarketPosition(const long id) { //--- In a loop by the list of current positions in the terminal for(int i=::PositionsTotal()-1; i>=0; i--) { //--- get the position ticket by the loop index ulong ticket=::PositionGetTicket(i); //--- If the ticket is received, the position can be selected and its ID is equal to the one passed to the method, //--- this is the desired market position, return 'true' if(ticket!=0 && ::PositionSelectByTicket(ticket) && ::PositionGetInteger(POSITION_IDENTIFIER)==id) return true; } //--- No such market position, return 'false' return false; }
在根据交易列表创建仓位列表时,重要的是不要考虑当前的市场仓位,并避免将它们添加到历史仓位列表中。要判断一个仓位是否尚未平仓,我们需要在活跃仓位列表中通过其ID查找它。如果这样的仓位存在,则无需将其添加到列表中。该方法通过仓位的票据(ticket)在市场仓位列表中搜索仓位,检查仓位ID是否与传递给方法的值匹配,如果这样的仓位存在(即可以被选中),则返回true。否则,返回false。
返回列表中第一个已平仓仓位的指针的方法:
//+------------------------------------------------------------------+ //| Return the pointer to the first closed position in the list | //+------------------------------------------------------------------+ CPosition *CPositionsControl::GetFirstClosedPosition(void) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); return this.m_list_pos.At(0); }
仓位列表按头寸平仓时间的毫秒数进行排序,并返回指向列表中第一个仓位(即最早的仓位)的指针。
返回列表中最后一个已平仓仓位的指针的方法:
//+------------------------------------------------------------------+ //| Return the pointer to the last closed position in the list | //+------------------------------------------------------------------+ CPosition *CPositionsControl::GetLastClosedPosition(void) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); return this.m_list_pos.At(this.m_list_pos.Total()-1); }
仓位列表按头寸平仓时间的毫秒数进行排序,并返回指向列表中最后一个仓位的指针。
返回列表中前一个已平仓仓位的指针的方法:
//+------------------------------------------------------------------+ //| Return the pointer to the previous closed position in the list | //+------------------------------------------------------------------+ CPosition *CPositionsControl::GetPrevClosedPosition(CPosition *current) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); int prev=this.m_list_pos.SearchLess(current); return this.m_list_pos.At(prev); }
基于当前选中的仓位(其指针被传递给该方法),在按平仓时间毫秒数排序的列表中搜索前一个仓位。CArrayObj类的SearchLess()方法返回列表中遇到的第一个对象的指针,该对象具有较低的排序值。在这种情况下,列表是按平仓时间的毫秒数进行排序的。因此,找到的第一个平仓时间小于传递给方法的平仓时间的对象,就是前一个仓位。如果未找到仓位对象,或者传递给方法的是列表的第一个元素(即没有前一个元素),则该方法返回NULL。
返回列表中下一个已平仓仓位的指针的方法:
//+------------------------------------------------------------------+ //| Return the pointer to the next closed position in the list | //+------------------------------------------------------------------+ CPosition *CPositionsControl::GetNextClosedPosition(CPosition *current) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); int next=this.m_list_pos.SearchGreat(current); return this.m_list_pos.At(next); }
基于当前选中的仓位,其指针被传递给该方法,然后在按平仓时间毫秒数排序的列表中搜索下一个仓位。CArrayObj类的SearchGreat()方法返回列表中遇到的第一个对象的指针,该对象具有较高的排序值,列表就是基于这个值进行排序的。在这种情况下,列表是按平仓时间的毫秒数进行排序的。因此,找到的第一个平仓时间大于传递给方法的平仓时间的对象,就是下一个仓位。如果未找到仓位对象,或者传递给方法的是列表的最后一个元素(即没有下一个元素),则该方法返回NULL。
在图表上显示给定仓位的图形表示的方法:
//+----------------------------------------------------------------------------+ //| Display a graphical representation of the specified position on the chart | //+----------------------------------------------------------------------------+ void CPositionsControl::Show(CPosition *pos,const bool chart_redraw=false) { if(pos!=NULL) { pos.Show(chart_redraw); this.m_current_id=pos.ID(); } }
该方法接收一个指向仓位的指针,该仓位的图形表示应该被显示在图表上。如果接收到的是一个有效的对象,则调用其Show()方法,并将该仓位的ID添加到m_current_id变量中。我们可以通过m_current_id变量中的仓位ID来定义当前选中的仓位。如果一个仓位的图形表示被显示在图表上,那么该仓位就被视为已选中。
从图表上隐藏第一个仓位的图形表示的方法:
//+------------------------------------------------------------------------+ //| Hide the graphical representation of the first position from the chart | //+------------------------------------------------------------------------+ void CPositionsControl::HideFirst(const bool chart_redraw=false) { CPosition *pos=this.GetFirstClosedPosition(); if(pos!=NULL) pos.Hide(chart_redraw); }
获取指向列表中第一个仓位的指针并调用其 Hide() 方法。
在图表上显示第一个仓位的图形表示的方法:
//+---------------------------------------------------------------------------+ //| Display the graphical representation of the first position on the chart | //+---------------------------------------------------------------------------+ void CPositionsControl::ShowFirst(const bool chart_redraw=false) { //--- Get the pointer to the first closed position CPosition *pos=this.GetFirstClosedPosition(); if(pos==NULL) return; //--- Hide labels of all positions except the first one by its ID this.HideAllExceptOne(pos.ID()); //--- Display the labels of the first position on the chart and //--- center the chart by the labels of the currently selected position this.Show(pos,chart_redraw); this.CentersChartByCurrentSelected(); }
使用上面提到的 GetFirstClosedPosition() 方法获取按时间(毫秒)排序的列表中第一个仓位的指针。隐藏除第一个仓位之外的所有仓位的标签,显示第一个仓位,并将图表置于其交易标签的中心。
隐藏图表上最后一个仓位的图形表示的方法:
//+------------------------------------------------------------------+ //| Hide graphical representation of the last position from the chart| //+------------------------------------------------------------------+ void CPositionsControl::HideLast(const bool chart_redraw=false) { CPosition *pos=this.GetLastClosedPosition(); if(pos!=NULL) pos.Hide(chart_redraw); }
获取列表中最后一个仓位的指针,并调用其Hide()方法。
在图表上显示最后一个仓位的图形表示的方法:
//+-----------------------------------------------------------------------+ //| Display the graphical representation of the last position on the chart| //+-----------------------------------------------------------------------+ void CPositionsControl::ShowLast(const bool chart_redraw=false) { //--- Get the pointer to the last closed position CPosition *pos=this.GetLastClosedPosition(); if(pos==NULL) return; //--- Hide labels of all positions except the last one by its ID this.HideAllExceptOne(pos.ID(), false); //--- Display the labels of the last position on the chart and //--- center the chart by the labels of the currently selected position this.Show(pos,chart_redraw); this.CentersChartByCurrentSelected(); }
使用之前提到的GetLastClosedPosition()方法来获取按毫秒时间排序的列表中最后一个仓位的指针。隐藏除最后一个仓位之外的所有仓位的标签,显示最后一个仓位,并将图表中心定位在其交易标签上。
在图表上显示当前仓位的图形表示的方法:
//+--------------------------------------------------------------------------+ //| Display a graphical representation of the current position on the chart | //+--------------------------------------------------------------------------+ void CPositionsControl::ShowCurrent(const bool chart_redraw=false) { //--- Get a pointer to the currently selected closed position CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID()); if(curr==NULL) return; //--- Display the labels of the current position on the chart and //--- center the chart by the labels of the currently selected position this.Show(curr,chart_redraw); this.CentersChartByCurrentSelected(); }
获取ID设置在m_current_id变量中的仓位的指针,在图表上显示其图形表示,并以其在图表上的交易标签为中心定位图表。
在图表上显示前一个仓位的图形表示的方法:
//+------------------------------------------------------------------------+ //|Display a graphical representation of the previous position on the chart| //+------------------------------------------------------------------------+ void CPositionsControl::ShowPrev(const bool chart_redraw=false) { //--- Get the pointer to the current and previous positions CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID()); CPosition *prev=this.GetPrevClosedPosition(curr); if(curr==NULL || prev==NULL) return; //--- Hide the current position, display the previous one and //--- center the chart by the labels of the currently selected position curr.Hide(); this.Show(prev,chart_redraw); this.CentersChartByCurrentSelected(); }
在这里,我们获取了列表中当前选中仓位和前一个仓位的指针。隐藏当前仓位的标签,并显示前一个仓位的标签。当它们被显示时,将仓位ID设置为m_current_id变量,表明这个仓位现在是当前选中的。使用其标签来将图表居中。
在图表上显示下一个仓位的图形表示的方法:
//+---------------------------------------------------------------------+ //| Display a graphical representation of the next position on the chart| //+---------------------------------------------------------------------+ void CPositionsControl::ShowNext(const bool chart_redraw=false) { //--- Get the pointer to the current and next positions CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID()); CPosition *next=this.GetNextClosedPosition(curr); if(curr==NULL || next==NULL) return; //--- Hide the current position, display the next one and //--- center the chart by the labels of the currently selected position curr.Hide(); this.Show(next,chart_redraw); this.CentersChartByCurrentSelected(); }
该方法与之前的方法相同,不同之处在于这里我们获取的是列表中当前选中仓位和下一个仓位的指针。隐藏当前仓位的标签,并显示下一个仓位的图标。当它们被显示时,将仓位ID设置为m_current_id变量,表明这个仓位现在是当前选中的。使用其标签来将图表居中。
隐藏图表上除指定仓位外所有仓位的图形表示的方法:
//+------------------------------------------------------------------+ //| Hide the graphical representation | //| of all positions except the specified one | //+------------------------------------------------------------------+ void CPositionsControl::HideAllExceptOne(const long pos_id,const bool chart_redraw=false) { //--- In a loop by the list of positions int total=this.m_list_pos.Total(); for(int i=0; i<total; i++) { //--- get the pointer to the next position and CPosition *pos=this.m_list_pos.At(i); if(pos==NULL || pos.ID()==pos_id) continue; //--- hide the graphical representation of the position pos.Hide(); } //--- After the loop, update the chart if the flag is set if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
该方法接收需要保留在图表上标签的仓位ID。所有其他仓位的标签都被隐藏。
将图表以当前选中仓位为中心的方法:
//+------------------------------------------------------------------+ //| Center the chart at the currently selected position | //+------------------------------------------------------------------+ void CPositionsControl::CentersChartByCurrentSelected(void) { //--- Get the index of the first visible bar on the chart and the number of visible bars int bar_open=0, bar_close=0; int first_visible=(int)::ChartGetInteger(this.m_chart_id, CHART_FIRST_VISIBLE_BAR); int visible_bars =(int)::ChartGetInteger(this.m_chart_id, CHART_VISIBLE_BARS); //--- Get the position opening time and use it to get the opening bar datetime time_open=this.TimeOpenCurrentSelected(); if(time_open!=0) bar_open=::iBarShift(this.m_symbol, PERIOD_CURRENT, time_open); //--- Get the position opening time and use it to get the closing bar datetime time_close=this.TimeCloseCurrentSelected(); if(time_close!=0) bar_close=::iBarShift(this.m_symbol, PERIOD_CURRENT, time_close); //--- Calculate the width of the window the deal labels are located in int width=bar_open-bar_close; //--- Calculate the chart offset so that the window with deals is in the center of the chart int shift=(bar_open + visible_bars/2 - width/2); //--- If the window width is greater than the chart width, the opening deal is located on the second visible bar if(shift-bar_open<0) shift=bar_open+1; //--- If the deal opening bar is to the left of the first visible bar of the chart //--- or the deal opening bar is to the right of the chart last visible bar, //--- scroll the chart by the calculated offset if(bar_open>first_visible || bar_open<first_visible+visible_bars) ::ChartNavigate(this.m_chart_id, CHART_CURRENT_POS, first_visible-shift); }
整个方法的逻辑在代码注释中得到了完整的描述。该方法移动交易品种图表,以便视觉上所有仓位的交易及其连接线都位于图表的中心。如果所有交易的标签无法在图表宽度内完全显示,则图表会移动,使得仓位开仓交易位于图表从左数第二个可见K线上。
返回当前选中仓位开仓时间的方法:
//+------------------------------------------------------------------+ //| Return the opening time of the currently selected position | //+------------------------------------------------------------------+ datetime CPositionsControl::TimeOpenCurrentSelected(void) { CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID()); return(pos!=NULL ? pos.Time() : 0); }
通过设置在m_current_id变量中的ID获取当前选中仓位的指针。如果成功接收到指针,则返回仓位的开仓时间。否则,返回零。
返回当前选中仓位平仓时间的方法:
//+------------------------------------------------------------------+ //| Return the current selected position closing time | //+------------------------------------------------------------------+ datetime CPositionsControl::TimeCloseCurrentSelected(void) { CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID()); return(pos!=NULL ? pos.TimeClose() : 0); }
通过设置在m_current_id变量中的ID获取当前选中仓位的指针。如果成功接收到指针,则返回仓位的平仓时间。否则,返回零。
通过交易类型返回仓位类型的方法:
//+------------------------------------------------------------------+ //| Return position type by deal type | //+------------------------------------------------------------------+ ENUM_POSITION_TYPE CPositionsControl::PositionTypeByDeal(const CDeal *deal) { if(deal==NULL) return WRONG_VALUE; switch(deal.TypeDeal()) { case DEAL_TYPE_BUY : return POSITION_TYPE_BUY; case DEAL_TYPE_SELL : return POSITION_TYPE_SELL; default : return WRONG_VALUE; } }
该方法接收交易的指针。根据交易类型,返回相应的头寸类型。
创建历史头寸列表的方法:
//+------------------------------------------------------------------+ //| Create historical position list | //+------------------------------------------------------------------+ bool CPositionsControl::Refresh(void) { //--- If failed to request the history of deals and orders, return 'false' if(!::HistorySelect(0,::TimeCurrent())) return false; //--- Set the flag of sorting by time in milliseconds for the position list this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_MSC); //--- Declare a result variable and a pointer to the position object bool res=true; CPosition *pos=NULL; //--- In a loop based on the number of history deals int total=::HistoryDealsTotal(); for(int i=total-1; i>=0; i--) { //--- get the ticket of the next deal in the list ulong ticket=::HistoryDealGetTicket(i); //--- If the deal ticket is not received, or it is not a buy/sell deal, or if the deal is not for the symbol set for the class, move on ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE); if(ticket==0 || (deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL) || ::HistoryDealGetString(ticket, DEAL_SYMBOL)!=this.m_symbol) continue; //--- Get the value of the position ID from the deal long pos_id=::HistoryDealGetInteger(ticket, DEAL_POSITION_ID); //--- If this is a market position, move on if(this.IsMarketPosition(pos_id)) continue; //--- Get the pointer to a position object from the list pos=this.GetPositionObjByID(pos_id); //--- If there is no position with this ID in the list yet if(pos==NULL) { //--- Create a new position object and, if the object could not be created, add 'false' to the 'res' variable and move on pos=new CPosition(pos_id, this.m_symbol); if(pos==NULL) { res &=false; continue; } //--- If failed to add the position object to the list, add 'false' to the 'res' variable, remove the position object and move on if(!this.m_list_pos.InsertSort(pos)) { res &=false; delete pos; continue; } } //--- If the deal object could not be added to the list of deals of the position object, add 'false' to the 'res' variable and move on CDeal *deal=pos.DealAdd(ticket); if(deal==NULL) { res &=false; continue; } //--- All is successful. //--- Set position properties depending on the deal type if(deal.Entry()==DEAL_ENTRY_IN) { pos.SetTime(deal.Time()); pos.SetTimeMsc(deal.TimeMsc()); ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal); pos.SetTypePosition(type); pos.SetPriceOpen(deal.Price()); pos.SetVolume(deal.Volume()); } if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) { pos.SetPriceCurrent(deal.Price()); } if(deal.Entry()==DEAL_ENTRY_INOUT) { ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal); pos.SetTypePosition(type); pos.SetVolume(deal.Volume()-pos.Volume()); } } //--- Set the flag of sorting by close time in milliseconds for the position list this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); //--- Return the result of creating and adding a position to the list return res; }
代码注释中详细讲述了方法逻辑。
让我们简要地看一下查找历史仓位的逻辑:客户端仅包含当前仓位的列表。每个仓位都有其自己的交易,这些交易位于历史交易列表中。每个交易都包含它所参与的仓位的ID。这个ID可以用来确定一个交易所属的头寸。因此,为了创建一个历史头寸列表,我们需要遍历历史交易列表,并使用ID来确定每个交易所属的头寸。请确保检查具有该ID的头寸是否不在活动头寸列表中。如果在,那么这意味着它还不是一个已平仓的头寸,应该跳过该交易。如果这样的头寸已经不在市场上,那么我们需要使用该ID创建一个新的历史头寸对象,并将该交易添加到新创建头寸的交易列表中。在创建头寸对象之前,我们需要检查是否已经创建了具有该ID的头寸。如果已经创建,那么就没有必要再创建一个新的头寸对象。相反,只需将该交易添加到已经创建的头寸的交易列表中。当然,如果这样的交易已经存在于头寸列表中,那么就没有必要再添加它。在完成历史交易循环后,将生成一个已平仓头寸的列表作为结果,并且该列表中的每个头寸都将包含其交易列表。
用于在日志中打印头寸及其交易属性的方法:
//+------------------------------------------------------------------+ //| Print the properties of positions and their deals in the journal | //+------------------------------------------------------------------+ void CPositionsControl::Print(void) { int total=this.m_list_pos.Total(); for(int i=0; i<total; i++) { CPosition *pos=this.m_list_pos.At(i); if(pos==NULL) continue; pos.Print(); } }
从最早的头寸(即列表的最开始位置)开始,我们依次获取每个后续头寸,并在循环中将其描述输出到日志中。
返回每个选定头寸描述的方法:
//+------------------------------------------------------------------+ //| Return the description of the currently selected position | //+------------------------------------------------------------------+ string CPositionsControl::CurrentSelectedDescription(void) { CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID()); return(pos!=NULL ? pos.Tooltip() : NULL); }
获取当前选定头寸的指针,其ID已设置在m_current_id变量中,并返回用于显示提示条的字符串。此字符串可用于在图表注释中显示。让我们在测试EA中展示它,以便在图表上直观地显示某些头寸的属性,这些属性的标签会显示在图表上。测试EA将包含以下功能:
- 启动后,交易历史以历史头寸列表的形式创建。每个头寸都包含其交易列表;
- 历史头寸列表完成后,最后一个已平仓的头寸将在图表上以开盘和收盘标签(由线连接)的形式显示;
- 您可以按住Ctrl键,使用光标键浏览历史头寸列表:
- 左键 - 在图表上显示前一个头寸;
- 右键 - 在图表上显示下一个头寸;
- 上键 - 在图表上显示第一个头寸;
- 下键 - 在图表上显示最后一个头寸;
- 按住Shift键可在图表上显示描述当前选定头寸的注释。
测试
在\MQL5\Experts\PositionsViewer\路径下,创建一个新的EA文件PositionViewer.mq5。
包含历史头寸管理类文件,声明用于按键代码的宏替换和EA全局变量:
//+------------------------------------------------------------------+ //| PositionViewer.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include <PositionsViewer\PositionsControl.mqh> #define KEY_LEFT 37 #define KEY_RIGHT 39 #define KEY_UP 38 #define KEY_DOWN 40 //--- global variables CPositionsControl ExtPositions; // Historical position class instance bool ExtChartScroll; // Chart scrolling flag bool ExtChartHistory; // Trading history display flag //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+
在EA的OnInit()处理程序中,您需要设置以下代码:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Save the chart auto scroll flag and disable auto scroll ExtChartScroll=ChartGetInteger(ChartID(), CHART_AUTOSCROLL); ChartSetInteger(ChartID(), CHART_AUTOSCROLL, false); //--- Save the trading history display flag and disable history display ExtChartHistory=ChartGetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY); ChartSetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY, false); //--- Create a list of closed positions and display the list creation time in the journal ulong start=GetTickCount64(); Print("Reading trade history and creating a list of historical positions"); ExtPositions.Refresh(); ulong msec=GetTickCount64()-start; PrintFormat("List of historical positions created in %I64u msec", msec); //ExtPositions.Print(); //--- If this is a launch after changing the chart period, display the currently selected position if(UninitializeReason()==REASON_CHARTCHANGE) ExtPositions.ShowCurrent(true); //--- otherwise, display the last closed position else ExtPositions.ShowLast(true); //--- Successful return(INIT_SUCCEEDED); }
此处保存了图表自动滚动和交易历史显示的标志,以便在程序平仓时恢复它们。图表自动滚动和交易历史显示被禁用,创建了当前交易品种上所有曾经存在的历史头寸列表,并在日志中显示了列表的创建时间。如果我们取消注释字符串//ExtPositions.Print();,在创建已平仓头寸列表后,将从创建的列表中显示所有历史头寸到日志中。如果这不是图表时间框架的更改,则将在图表上绘制最后一个已平仓头寸的标签,并将图表滚动以使标签位于其中心。如果这是图表周期的更改,则显示列表中当前选定的头寸。
在OnDeinit()处理程序中,恢复图表自动滚动和交易历史显示保存的值,并从图表中删除注释:
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Restore the auto scroll and trading history property initial value and remove chart comments ChartSetInteger(ChartID(), CHART_AUTOSCROLL, ExtChartScroll); ChartSetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY, ExtChartHistory); Comment(""); }
在OnTradeTransaction()处理程序中,每次发生新交易时,调用历史头寸控制类的列表更新方法。如果头寸已平仓,则在图表上显示其标签:
//+------------------------------------------------------------------+ //| TradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { //--- In case of a transaction type, add a new deal if(trans.type==TRADE_TRANSACTION_DEAL_ADD) { //--- update the list of positions and their deals ExtPositions.Refresh(); //--- Get the new deal ticket ulong deal_ticket=trans.deal; //--- If the ticket is not received or failed to get the method for updating the position from the deal properties, leave long entry; if(deal_ticket==0 || !HistoryDealGetInteger(deal_ticket, DEAL_ENTRY, entry)) return; //--- If this is an exit deal, display the last closed position on the chart if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_OUT_BY) ExtPositions.ShowLast(true); } }
在事件处理程序中,跟踪按键事件,并对在按住Ctrl键的同时按下的光标键作出响应,以便在已平仓头寸列表中导航。此外,对Shift键作出响应,在图表注释中显示头寸描述,以及对改变图表比例以根据当前头寸标签居中图表作出响应:
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- If the event ID is pressing a key if(id==CHARTEVENT_KEYDOWN) { //--- If the Ctrl key is held down if(TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)<0) { //--- If the chart scrolling with keys is active, disable it if((bool)ChartGetInteger(0, CHART_KEYBOARD_CONTROL)) ChartSetInteger(0, CHART_KEYBOARD_CONTROL, false); //--- If the left key is pressed, display the previous closed position if(lparam==KEY_LEFT) ExtPositions.ShowPrev(true); //--- If the right key is pressed, display the next closed position if(lparam==KEY_RIGHT) ExtPositions.ShowNext(true); //--- If the up key is pressed, display the first closed position if(lparam==KEY_UP) ExtPositions.ShowFirst(true); //--- If the down key is pressed, display the last closed position if(lparam==KEY_DOWN) ExtPositions.ShowLast(true); } //--- If Ctrl is not pressed, else { //--- If the chart scrolling with keys is inactive, enable it if(!(bool)ChartGetInteger(0, CHART_KEYBOARD_CONTROL)) ChartSetInteger(0, CHART_KEYBOARD_CONTROL, true); } } //--- If the Shift key is held down, display a description of the current position in the chart comment if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<0) Comment(ExtPositions.CurrentSelectedDescription()); //--- If the Shift key is not pressed, check the comment on the chart and delete it if it is not empty else { if(ChartGetString(ChartID(),CHART_COMMENT)!=NULL) Comment(""); } //--- If the horizontal scale of the chart has changed, display the currently selected position static int scale=-1; if(id==CHARTEVENT_CHART_CHANGE) { int scale_curr=(int)ChartGetInteger(ChartID(), CHART_SCALE); if(scale!=scale_curr) { ExtPositions.ShowCurrent(true); scale=scale_curr; } } }
这是已平仓头寸的排列方式,同时展示了交易历史的标准显示:
以下是对文章开头所写内容的阐释:
现在让我们编译EA并在图表上启动它:
在启动时,通过标准方式显示的历史被隐藏,只有最后一个已平仓头寸保持可见。我们可以通过按住Ctrl键并按下光标键来浏览头寸历史。在浏览已平仓头寸列表时,我们可以很容易地在图表上看到交易历史的图形表示,在这里图表上原本充满了所有平仓头寸交易的标签。
如果我们还按住Shift键,当前选中的平仓头寸的描述将显示在图表注释中。
现在让我们找到两个彼此距离足够远的头寸,看看如果它们无法在图表窗口的可见区域显示下,增加图表的水平比例尺时,它们是如何在图表上居中的:
我们看到,如果两笔交易都不适配图表窗口,那么图表会居中显示,以便可以看到从左数第二根柱子上进入头寸的交易。
如果我们按下Shift键,我们将在图表注释中看到此已平仓头寸的描述:
这很方便,因为它允许我们在不将鼠标光标悬停在连接交易的线条上方的情况下看到头寸的描述。如果我们使用光标键浏览已平仓头寸列表,同时按住Ctrl + Shift键,我们可以清楚地看到当前在图表上显示的已平仓头寸的描述。
启动EA将开始创建历史头寸列表。在我的例子中,由于我的交易历史较短(从2023年开始),列表的创建时间为:
PositionViewer (EURUSD,M15) 读取交易历史并创建历史仓位列表
PositionViewer (EURUSD,M15) 在6422毫秒内创建了历史仓位列表
随后切换图表周期不再需要时间来重新创建列表:
PositionViewer (EURUSD,M1) 正在读取交易历史并创建历史仓位列表 PositionViewer (EURUSD,M1) 在31毫秒内创建了历史仓位列表 PositionViewer (EURUSD,M5) 正在读取交易历史并创建历史仓位列表 PositionViewer (EURUSD,M5) 在47毫秒内创建了历史仓位列表 PositionViewer (EURUSD,M1) 正在读取交易历史并创建历史仓位列表 PositionViewer (EURUSD,M1) 在 31毫秒内创建历史头寸列表
结论
在活跃频繁的交易中,由本程序提供的交易历史展示方式将更为便捷——图表上永远不会因显示已平头寸的标签而显得过于拥挤——每个头寸都会独立显示,并且可以通过光标键在当前头寸与下一个或上一个头寸之间进行切换。每个交易标签的提示条中包含的信息比标准的交易历史更为丰富。将鼠标悬停在连接交易的线条上,将显示一个包含已平头寸信息的工具提示。
文章中创建的类可以用于我们自己的开发中,扩展功能并创建更复杂且实用的程序,这些程序包含交易历史和已平头寸历史的信息。
所有类文件和测试EA都已附加到文章中,可供独立学习。MQL5.zip归档文件包含的文件可以直接解压到MQL5终端目录中,并且将在Experts文件夹中创建一个新的PositionsViewer\文件夹,其中包含所有项目文件:PositionViewer.mq5 EA文件和三个包含的类文件。我们可以自由地编译EA文件并使用它,以便在交易过程中方便地查看和管理我们的头寸。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/15026